MIGUEL Á. TOLEDO MARTÍNEZ CONTENIDO DE LA LECCIÓN 20 APUNTADORES Y CADENAS 1. Introducción 2. Declaración e iniciación de variables de apuntador 3. Operadores de los apuntadores 3.1. Ejemplo 20.1 4. Llamado de funciones por referencia 4.1. Ejemplo 20.2 5. Empleo del calificador const con apuntadores 5.1. Ejemplo 2 20.3, 20.4, 20.5, 20.6, 20.7 6. Ordenamiento de burbuja mediante llamada por referencia 6.1. Ejemplos 20.8, 20.9, 20.10 7. Expresiones de apuntadores y aritmética de apuntadores 8. Relación entre apuntadores y arreglos 8.1. Ejemplos 20.11, 20.12 9. Arreglos de apuntadores 10. Caso de estudio: Simulación de barajado y repartición de naipes 10.1. Ejemplo 20.13 3 37 4 6 6 7 10 11 15 15 18 21 22 24 25 28 11. Apuntadores y funciones 29 11.1. Ejemplos 20.14, 20.15 29 12. Introducción al procesamiento de caracteres y cadenas 12.1. Fundamentos de los caracteres y las cadenas 12.2. Funciones de manipulación de cadenas de la biblioteca de manejo de cadenas 12.2.1. Ejemplo 20.16, 20.17, 20.18, 20.19, 20.20 12.3. Ejercicios resueltos 12.4. Funciones de cadena que utilizan cadenas ubicadas fuera de los 64 kb (far string) 12.5. Número de ocurrencias de un carácter dentro de una cadena 12.6. Contar el número de ocurrencia de una subcadena dentro de una cadena 12.7. Obtener un índice a una subcadena 12.8. Obtener la ocurrencia mas a la derecha de una subcadena 12.9. Remover una subcadena contenida dentro de una cadena 12.10. Reemplazo de una subcadena por otra 12.11. Determinar si un carácter es alfanumérico 12.12. Determinar si un carácter es una letra del alfabeto 12.13. Determinar si un carácter contiene un valor ASCII 12.14. Determinar si un carácter es un carácter de control 12.15. Determinar si un carácter es un digito 12.16. Determinar si un carácter es un carácter gráfico 12.17. Determinar si un carácter es mayúscula o minúscula 12.18. Determinar si un carácter es imprimible 12.19. Determinar si un carácter es un símbolo de puntuación 12.20. Determinar si un carácter es el carácter espacio 12.21. Determinar si un carácter es un valor hexadecimal 12.22. Carácter ASCII válido 13. Pensando en objetos: Iteraciones entre los objetos 14. Errores comunes de programación 15. Buenas prácticas de programación 16. Propuestas de desempeño APUNTADORES Y CADENA – LECCIÓN 20 33 33 34 36 40 47 47 53 54 54 54 55 56 56 56 56 56 57 57 57 57 58 58 59 64 65 66 67 20-1 MIGUEL Á. TOLEDO MARTÍNEZ 17. Sugerencias de portabilidad 18. Observaciones de Ingeniería de Software 19. Indicaciones de prueba y depuración 20. Lo que necesita saber 21. Preguntas y problemas 21.1. Preguntas 21.2. Problemas 21.2.1. 21.2.2. 21.2.3. 21.2.4. 21.2.5. Sección especial: construya su propia computadora Más problemas de apuntadores Problemas de manipulación de cadenas Sección especial: manipulación avanzada de cadenas Interesante proyecto de manipulación de cadenas APUNTADORES Y CADENA – LECCIÓN 20 67 67 67 68 70 70 73 75 79 84 85 88 20-2 MIGUEL Á. TOLEDO MARTÍNEZ LECCIÓN 20 APUNTADORES Y CADENAS INTRODUCCIÓN En esta lección se estudiará una de las características más poderosas del lenguaje de programación C++: el apuntador. Los apuntadores son una de las capacidades de C++ más difíciles de dominar. En otra lección vimos que las referencias pueden servir para hacer llamadas por referencia. Los apuntadores permiten a los programas simular la llamada por referencia y crear y manipular estructuras dinámicas de datos, es decir, estructuras de datos que pueden crecer y encogerse, como las listas vinculadas, colas, pilas y árboles. Esta lección explica los conceptos básicos acerca de los apuntadores. También refuerza la relación íntima entre los arreglos, los apuntadores y las cadenas e incluye un amplio conjunto de ejemplos de procesamiento de cadenas. En otro semestre se estudiarán el empleo de los apuntadores con estructuras. La programación orientada a objetos se efectúa con apuntadores y referencias. En ese semestre se presentan técnicas de administración dinámica de memoria y ejemplos de creación y uso de estructuras dinámicas de datos. El enfoque de los arreglos y cadenas como apuntadores se deriva de C. En la especialidad de computación se estudiarán los arreglos y las cadenas como objetos en toda forma. Los objetivos de esta lección son: • • • • • • • • • • Aprender a utilizar los apuntadores. Emplear los apuntadores para pasar argumentos a las funciones mediante llamada por referencia. Entender la estrecha relación entre los apuntadores, los arreglos y las cadenas. Comprender la utilidad de los apuntadores a funciones. Declarar y utilizar arreglos de cadenas. Asignar caracteres a una cadena de caracteres. Comprender la importancia del carácter NULL. Inicializar cadena de caracteres. Pasar cadena de caracteres a una función. Utilizar las funciones de las librerías en tiempo de ejecución para manejar cadenas de caracteres. DECLARACIÓN E INICIACIÓN DE VARIABLES DE APUNTADOR Las variables de apuntador contienen direcciones de memoria como sus valores. Normalmente las variables contienen valores específicos. Por otra parte, los apuntadores contienen direcciones de variables que contienen valores específicos. En este sentido, los nombres de variables hacen referencia directa a un valor y los apuntadores hacen referencia indirecta a un valor (figura 20.1) La referencia a un valor a través de un apuntador se llama indirección. APUNTADORES Y CADENA – LECCIÓN 20 20-3 MIGUEL Á. TOLEDO MARTÍNEZ contador 7 contadorPtr • contador hace referencia directa a una variable cuyo valor es 7 contador 7 contadorPtr hace referencia indirecta a una variable cuyo valor es 7. Figura 20.1. Referencias directa e indirecta a una variable Los apuntadores, como cualquier otra variable, se deben declarar antes de utilizarlos. La declaración: int *contadorPtr, contador; declara la variable contadorPtr como de tipo int * (es decir, como un apuntador a un valor entero) y se lee contadorPtr es un apuntador a int o contadorPtr apunta a un objeto de clase entero. Además, la variable contador se declara como entero, no como apuntador a un entero. En esta declaración, el * solamente se aplica a contadorPtr. Cada variable que se declara como apuntador debe ir precedida por un asterisco (*) Por ejemplo, la declaración: float *xPtr, *yPtr; indica que tanto xPtr como yPtr son apuntadores a valores float. El uso de * en una declaración de esta manera indica que la variable que se está declarando es un apuntador. Los apuntadores pueden declararse para que apunten a objetos de cualquier clase de datos. Los apuntadores se deben inicializar, ya sea al declararlos o mediante una instrucción de asignación. Un apuntador puede inicializarse a 0, a NULL o a una dirección. Un apuntador con el valor 0 o NULL no apunta a nada. NULL es una constante simbólica definida en el archivo de encabezado <iostream.h> (y en varios archivos de encabezado de la biblioteca estándar) La iniciación a NULL de un apuntador es equivalente a inicializarlo a 0, pero en C++ se prefiere 0. Cuando se asigna 0, se convierte en un apuntador del tipo adecuado. El valor 0 es el único valor entero que puede asignarse directamente a una variable de apuntador sin convertir primero mediante cast el entero a un tipo de apuntador. En la siguiente sección se estudia la asignación de la dirección de una variable a un apuntador. OPERADORES DE LOS APUNTADORES El &, u operador de dirección, es un aperador unario que devuelve la dirección de su operando. Por ejemplo, suponiendo las declaraciones: int y = 5; int *yPtr; la instrucción yPtr = &y; APUNTADORES Y CADENA – LECCIÓN 20 20-4 MIGUEL Á. TOLEDO MARTÍNEZ asigna la dirección de la variable y a la variable de apuntador yPtr. Se dice entonces que la variable yPtr apunta a y. La figura 20.2 muestra una representación esquemática de la memoria tras la ejecución de la asignación previa. En la figura mostramos la relación de apuntador dibujando una flecha del apuntador hacia el objeto al que apunta. y 5 yPtr • Figura 20.2. Representación gráfica de un apuntador que apunta a una variable entera en la memoria La figura 20.3 muestra la representación del apuntador en la memoria, suponiendo que la variable entera y está almacenada en la localidad 600000 y que la variable de apuntador yPtr está almacenada en la localidad 500000. El operando del operador de dirección debe ser un lvalue (left value) (es decir, algo a lo que puede asignarse un valor, como un nombre de variable); el operador de dirección no puede aplicarse a constantes, a expresiones que no den como resultado referencias, ni a variables declaradas con la clase de almacenamiento register. yPtr 500000 y 600000 600000 5 El operador *, conocido comúnmente como operador de indirección u operador de desreferenciación, devuelve un sinónimo, alias o apodo del objeto hacia el que apunta su operando (es decir, su apuntador) Por ejemplo (haciendo referencia a la figura 20.2), la instrucción: cout << *yPtr << endl; imprime el valor de la variable y, es decir 5, de la misma manera que lo haría la instrucción: cout << y << endl; El empleo de * de esta manera se conoce como desreferenciación de un apuntador. Observe que también puede utilizarse un apuntador desreferenciado del lado izquierdo de una instrucción de asignación, como: *yPtr = 9; que asignará 9 a y en la figura 20.3. El apuntador desreferenciado también podría usarse para recibir un valor de entrada como en el caso de: cin >> *yPtr; El apuntador desreferenciado es un lvalue, o valor izquierdo. APUNTADORES Y CADENA – LECCIÓN 20 20-5 MIGUEL Á. TOLEDO MARTÍNEZ Ejemplo 20.1 El programa siguiente: APUNTADOR1.CPP, muestra los operadores de los apuntadores. En este ejemplo, las localidades de memoria se envían a la salida como enteros hexadecimales. /* El siguiente programa: APUNTADOR1.CPP, muestra el empleo de los operadores & y * */ #include <iostream.h> //Para cout y cin void main(void) { int a; int *aPtr; //a es un entero //aPtr es un apuntador a un entero a = 7; aPtr = &a; //aPtr se establece a la dirección de a cout << "La dirección de a es: " << &a << "\nEl valor de aPtr es: " << aPtr; cout << "\n\nEl valor de a es: " << a << "\nEl valor de *Ptr es: " << *aPtr; cout << "\n\nMostrando que * y & son inversos " << "entre sí. \n&*aPtr = " << &*aPtr << "\n*&aPtr = " << *&aPtr << endl; }//Fin de main() Observe que la dirección de a y el valor de aPtr son idénticos en la salida, lo que confirma que la dirección de a efectivamente está asignada a la variable de apuntador aPtr. Los operadores & y * son inversos entre ellos –cuando se aplican ambos consecutivamente a aPtr en cualquier orden, se imprime el mismo resultado. LLAMADO DE FUNCIONES POR REFERENCIA En C++ hay tres maneras de pasarle argumentos a una función: mediante llamada por valor, mediante llamada por referencia con argumentos de referencia y mediante llamada por referencia con argumentos de apuntador. En lección anterior comparamos e hicimos la distinción entre llamada por valor y llamada por referencia con argumentos de referencia. En esta lección nos concentraremos en la llamada por referencia con argumentos de apuntador. Como antes hemos visto, return puede utilizarse para devolverle al invocador un valor desde una función llamada (o regresar el control desde una función llamada sin devolver un valor) También vimos que se le pueden pasar argumentos a una función mediante argumentos de referencia, permitiendo que la función modifique los valores originales de los argumentos (por lo tanto, se puede devolver más de un valor desde una función), o pasar objetos de datos grandes a una función y evitar la sobrecarga de pasar los objetos mediante llamada por valor (que, claro está, involucra llevar a cabo una copia del objeto) Los apuntadores, como las referencias, también pueden servir para modificar una o más variables del invocador o para pasar apuntadores a objetos de datos grandes, evitando la sobrecarga de pasar los objetos mediante llamada por valor. APUNTADORES Y CADENA – LECCIÓN 20 20-6 MIGUEL Á. TOLEDO MARTÍNEZ En C++, los programadores se pueden valer de los apuntadores y del operador de indirección para simular llamadas por referencia (de la misma manera que se logran las llamadas por referencia en C) Al llamar una función con argumentos que deben ser modificados, se pasa la dirección de los argumentos. Por lo general, esto se logra aplicándole el operador de dirección (&) al nombre de la variable que se habrá de modificar. Como hemos visto, los arreglos no se pasan mediante el operador &, pues el nombre de éstos es la localidad inicial del mismo en memoria (el nombre del arreglo es equivalente a &nombreArreglo[0], es decir, un nombre de arreglo ya es un apuntador. Cuando se pasa la dirección de una variable a una función, se puede emplear el operador de indirección (*) en la función, creando un sinónimo, alias o apodo del nombre de la variable –con éste, a su vez, es posible modificar el valor de la localidad de memoria del invocador (si la variable no está declarada como const) Ejemplo 20.2 Los dos programas siguientes: CUBO1.CPP y CUBO2.CPP, son dos versiones de una función que eleva al cubo un entero: cuboValor() y cuboReferencia() CUBO1.CPP, pasa la variable numero a la función cuboValor() mediante una llamada por valor. La función cuboReferencia() eleva al cubo su argumento y le devuelve el valor a main() mediante una instrucción return. El nuevo valor se asigna a numero en main() Existe la oportunidad de examinar el resultado de la llamada de función antes de modificar el valor de una variable. Por ejemplo, en este programa se podría haber almacenado el resultado de cuboValor() en otra variable, examinando su valor y asignando el resultado a numero tras comprobar que el resultado es razonable. /* El siguiente programa: CUBO1.CPP, eleva al cubo una variable mediante una llamada por valor. */ #include <iostream.h> //Para cout y cin int cuboValor(int); //Prototipo void main(void) { int numero = 5; cout << "El valor original del número es: " << numero; numero = cuboValor(numero); cout << "\nEl nuevo valor del número es: " << numero << endl; }//Fin de main() int cuboValor(int n) { return n * n * n; }//Fin de cuboValor() //Eleva al cubo la variable local n El programa CUBO2.CPP pasa la variable numero mediante una llamada por referencia (se pasa la dirección de numero) a la función cuboReferencia() Esta toma nPtr (que es un apuntador a int) como argumento. La función desreferencia el apuntador y eleva al cubo el valor al que apunta nPtr. Esto cambia el valor de numero en main() APUNTADORES Y CADENA – LECCIÓN 20 20-7 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: CUBO2.CPP, eleva al cubo una variable mediante una llamada por referencia, con un argumento de apuntador. */ #include <iostream.h> //Para cout y cin void cuboReferencia(int *); //Prototipo void main(void) { int numero = 5; cout << "El valor original del número es: " << numero; cuboReferencia(&numero); cout << "\nEl nuevo valor del número es: " << numero << endl; }//Fin de main() void cuboReferencia(int *nPtr) { *nPtr = *nPtr * *nPtr * *nPtr; }//Fin de cuboReferencia() //Eleva al cubo el número en main() Una función que recibe como argumento una dirección debe definir un parámetro de apuntador para recibir dicha dirección. Por ejemplo, el encabezado de la función cuboReferencia() es: void cuboReferencia(int *nPtr); El encabezado de la función especifica que cuboReferencia() contiene int * entre paréntesis. Como sucede con los demás tipos de variables, no es necesario incluir los nombres de los apuntadores en los prototipos de función. El compilador ignorará los nombres incluidos con fines de documentación. En el encabezado de la función y en el prototipo de una función que espera como argumento un arreglo de un solo índice, puede emplearse la notación de apuntadores en la lista de parámetros de cuboReferencia() El compilador no hace ninguna distinción entre una función que recibe un apuntador y otra que recibe un arreglo de un solo índice. Por supuesto, esto significa que la función debe saber cuándo está recibiendo un arreglo y cuándo se trata simplemente de una variable sencilla sobre la que debe efectuar una llamada por referencia. Cuando el compilador encuentra un parámetro de función para un arreglo de un solo índice, de la forma int b[], lo convierte a la notación de apuntadores int * const b (que se pronuncia como b es un apuntador constante a un entero); const se explica en la sección siguiente (empleo del calificador const con apuntadores) Ambas formas de declaración de un parámetro de función como arreglo de un solo índice son intercambiables. Las figuras 20.3 y 20.4 son análisis gráficos de los programas CUBO1.CPP y CUBO2.CPP. Antes de que main() llame a cuboValor(): int main() { int numero = 5; numero 5 int cuboValor(int n) { return n * n * n; }//Fin de cuboValor() n indefinido numero = cuboValor(numero); }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-8 MIGUEL Á. TOLEDO MARTÍNEZ Después de que cuboValor() recibe la llamada: int main() { int numero = 5; numero 5 int cuboValor(int n) { return n * n * n; }//Fin de cuboValor() n 5 numero = cuboValor(numero); }//Fin de main() Después de que cuboValor() eleve al cubo el parámetro n: int main() { int numero = 5; numero 5 int cuboValor(int n) { 125 return n * n * n; }//Fin de cuboValor() n int cuboValor(int n) { return n * n * n; }//Fin de cuboValor() n indefinido numero = cuboValor(numero); }//Fin de main() Después de que cuboValor() regresa a main(): int main() numero { 5 int numero = 5; 125 numero = cuboValor(numero); }//Fin de main() indefinido Después de que main() completa la asignación de numero: int main() { int numero = 5; numero 125 int cuboValor(int n) { return n * n * n; }//Fin de cuboValor() n indefinido numero = cuboValor(numero); }//Fin de main() Figura 20.3. Análisis de una llamada por valor típica. Antes de la llamada por referencia a cuboReferencia(): int main() { int numero = 5; numero 5 void cuboReferencia(int *nPtr) { nPtr *nPtr = * nPtr * *nPtr * *nPtr; indefinido }//Fin de cuboValor() cuboReferencia(&numero); }//Fin de main() Después de la llamada por referencia a cuboReferencia() y antes de elevar *nPtr al cubo: int main() { int numero = 5; numero 5 void cuboReferencia(int *nPtr) { nPtr *nPtr = * nPtr * *nPtr * *nPtr; }//Fin de cuboValor() • cuboReferencia(&numero); }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-9 MIGUEL Á. TOLEDO MARTÍNEZ Después de elevar *nPtr al cubo: int main() { int numero = 5; numero 125 void cuboReferencia(int *nPtr) { nPtr *nPtr = * nPtr * *nPtr * *nPtr; }//Fin de cuboValor() • cuboReferencia(&numero); }//Fin de main() Figura 20.4. Análisis de una llamada por referencia típica con argumento de apuntador. EMPLEO DEL CALIFICADOR const CON APUNTADORES El calificador const le permite al programador informarle al compilador que no se debe modificar el valor de una variable en particular. Existen seis posibilidades para utilizar (o no utilizar) const con parámetros de función – dos con paso de parámetros mediante llamada por valor y cuatro con paso de parámetros mediante llamada por referencia. ¿Cómo escoger entre las seis posibilidades? Sea la guía el principio de menor privilegio. Siempre habrá que dar a una función el suficiente acceso a la información de sus parámetros para que pueda llevar a cabo la tarea encomendada, pero no más. En lección anterior, explicamos que, cuando se invoca una función mediante llamada por valor, se obtiene una copia del argumento (o argumentos) en la llamada de función, la cual es la que pasa a la función. Si en la función se modifica la copia, el valor original en el invocador se conserva inalterado. En muchos casos, un valor pasado a una función es modificado para que la función pueda llevar a cabo su tarea. Sin embargo, en algunos casos, el valor no debe ser alterado en la función llamada, aún si la función llamada sólo manipula una copia del valor original. Considere una función que toma como argumentos un arreglo de un solo índice y su tamaño, y que imprime dicho arreglo. Tal función deberá recorrer el arreglo utilizando un ciclo y enviar a la salida, uno por uno, los elementos del mismo. El tamaño del arreglo se utiliza en el cuerpo de la función para determinar el índice más grande del arreglo y poder terminar el ciclo cuando se ha terminado la impresión. El tamaño del arreglo no cambia en el cuerpo de la función. Si un valor no cambia (o no debe cambiar) en el cuerpo de una función a la cual se pasa, el parámetro debe ser declarado como const con el fin de asegurar que no sea modificado por accidente. Si se intenta modificar una valor const, el compilador lo detecta y emite un aviso o un error, dependiendo del compilador. Hay cuatro maneras de pasar una apuntador a una función: con un apuntador no constante hacia datos no constantes, mediante un apuntador no constante hacia datos constantes, por medio de un apuntador constante hacia datos no constantes y utilizando un apuntador constante hacia datos constantes. Cada combinación ofrece un nivel distinto de privilegio de acceso. El mayor acceso se otorga mediante un apuntador no constante hacia datos no constantes –la información puede modificarse a través del apuntador desreferenciado y dicho apuntador se puede modificar para que apunte hacia otros datos. La declaración de apuntadores no constantes APUNTADORES Y CADENA – LECCIÓN 20 20-10 MIGUEL Á. TOLEDO MARTÍNEZ hacia datos no constante no incluye a const. Tal apuntador puede servir para recibir una cadena en una función que se vale de aritmética de apuntadores para procesar (y tal vez modificar) uno por uno los caracteres de la cadena. La función convertirMayuscula() en el programa MAYÚSCULA.CPP declara el parámetro sPtr (char *sPtr) como apuntador no constante hacia datos no constantes. La función procesa la cadena cadena, carácter por carácter, mediante aritmética de apuntadores. Los caracteres entre la a y la z son convertidos a sus letras mayúsculas a través de la función toupper(), los demás se conservan iguales. La función toupper() toma como argumento un carácter. Si dicho carácter es una letra minúscula, se devuelve la letra mayúscula correspondiente; de otro modo, se devuelve el carácter original. La función toupper() es parte de la biblioteca de manejo de caracteres ctype.h. Ejemplo 20.3 El siguiente programa: MAYÚSCULA.CPP, ilustra el uso de un apuntador no constante a datos no constantes. /* El siguiente programa: MAYUSCULA.CPP, convierte letras minúsculas a mayúsculas, por medio de un apuntador no constante hacia datos no constantes. */ #include <iostream.h> #include <ctype.h> //Para cout y cin //Para toupper() void convertirMayuscula(char *); void main(void) { char cadena[] = "caracteres y $32.98"; cout << "La cadena de caracteres antes de la conversión es : " << cadena; convertirMayuscula(cadena); cout << "\nLa cadena de caracteres después de la conversión es: " << cadena << endl; }//Fin de main() void convertirMayuscula(char *sPtr) { while(*sPtr != '\0') { if(*sPtr >= 'a' && *sPtr <= 'z') *sPtr = toupper(*sPtr); ++sPtr; }//Fin del while }//Fin de convertirMayuscula() //Convierte a mayúsculas //Mueve sPtr al siguiente carácter Un apuntador no constante hacia datos constantes es un apuntador que se puede modificar para que apunte hacia cualquier elemento de información del tipo apropiado, pero los datos a los que apunta no pueden modificarse por medio de ese apuntador. Tal apuntador podría servir para recibir como argumento un arreglo en una función que procesará cada elemento de dicho arreglo sin modificar los datos. APUNTADORES Y CADENA – LECCIÓN 20 20-11 MIGUEL Á. TOLEDO MARTÍNEZ Ejemplo 20.4 La función imprimirCaracteres() del programa IMPRIMIR.CPP, declara que los parámetros sPtr son del tipo const char *. La declaración se lee de derecha a izquierda como sPtr es un apuntador hacia una constante de caracteres. El cuerpo de la función utiliza una estructura for con la que envía a la salida, uno por uno, los caracteres de la cadena, hasta que encuentra el carácter nulo. Tras la impresión de un carácter, se incremente sPtr para apuntar al siguiente carácter de la cadena. /* El siguiente programa: IMPRIMIR.CPP, imprime una cadena, caracter tras caracter, mediante un apuntador no constante hacia datos constantes. */ #include <iostream.h> //Para cout y cin void imprimirCaracteres(const char *); void main(void) { char cadena[] = "imprime caracteres de una cadena"; cout << "La cadena es:\n"; imprimirCaracteres(cadena); cout << endl; }//Fin de main() /* En imprimirCaracteres(), sPtr es el apuntador hacia una constante de caracteres. Los caracteres no pueden modificarse por medio de sPtr(es decir, sPtr es un apuntador de solo lectura). */ void imprimirCaracteres(const char *sPtr) { for(; *sPtr != '\0'; sPtr++) // No hay inicialización cout << *sPtr; }//Fin de imprimirCaracteres() Ejemplo 20.5 El siguiente programa: MODIFICAR.CPP, muestra los mensajes de error que se generan al intentar compilar una función que recibe un apuntador hacia datos constantes y luego intenta emplear dicho apuntador para modificar los datos. /* El siguiente programa: MODIFICAR.CPP, intenta modificar la información por medio de un apuntador no constante hacia datos constantes. */ #include <iostream.h> //Para cout y cin void f(const int *); void main(void) { int y; f(&y); }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 //f intenta una modificación ilegal 20-12 MIGUEL Á. TOLEDO MARTÍNEZ // En f, xPtr es un apuntador hacia una constante entera void f(const int *xPtr) { *xPtr = 100; // No se puede modificar un objeto const }//Fin de f() Los mensajes que el compilador envía son los siguientes: Cannot modify a const object Parameter ‘xPtr’ is never used Como sabemos, los arreglos son tipos de datos agrupados que almacenan elementos de datos relacionados del mismo tipo bajo un mismo nombre. En otra lección se estudiará otra forma de tipo de datos agrupados, llamado estructura (a veces conocido como registro en otros lenguajes) Una estructura es capaz de almacenar elementos de datos relacionados de distintos tipos bajo un mismo nombre (por ejemplo, para almacenar información sobre todos los empleados de una compañía) Al invocar una función con un arreglo como argumento, éste se pasa de manera automática a la función simulando una llamada por referencia. Sin embargo, las estructuras siempre se pasan mediante llamada por valor –se pasa una copia de toda la estructura. Esto requiere la sobrecarga en tiempo de ejecución que es causada por copiar todos los elementos de datos de la estructura y almacenarlos en la pila de llamadas de función de la computadora (que es el lugar donde se almacenan las variables locales empleadas en la función mientras ésta se ejecuta) Cuando hay que pasar una estructura a una función, puede utilizarse un apuntador hacia datos constantes (o una referencia hacia datos constantes), logrando el desempeño de una llamada por referencia y la protección de una llamada por valor. Cuando el apuntador se pasa hacia una estructura, sólo hay que copiar la dirección en la que se almacena tal estructura. En una máquina con direcciones de 4 bytes, se hace una copia de 4 bytes de memoria, en lugar de los, tal vez, cientos o miles de bytes de la estructura. Un apuntador constante hacia datos no constantes es un apuntador que siempre apunta a la misma localidad de memoria; los datos de dicha localidad se pueden modificar a través del apuntador. Esto es lo predeterminada para un nombre de arreglo, el cual es un apuntador constante hacia el inicio del arreglo. Es posible acceder y cambiar toda la información del arreglo por medio de su nombre y sus índices. Un apuntador constante hacia datos no constantes puede servir para recibir como argumento un arreglo en una función que accede a elementos del mismo, empleado sólo notación de índices. Los apuntadores que se declaran como const deben ser inicializados cuando se declaran (si el apuntador es un parámetro de función, se inicializa con un apuntador que se pasa a la función) Ejemplo 20.6 El programa MODIFICAR2.CPP, intenta modificar un apuntador constante. El apuntador ptr se declara como de tipo int * const. Esta declaración se lee de derecha a izquierda como ptr es un apuntador constante hacia un entero. El apuntador se inicializa con la dirección de la variable entera x. El programa intenta asignarle a ptr la dirección de y, lo cual genera un mensaje de error. Observe que al asignarle el valor 7 a *ptr no se produce un error –el valor al que apunta ptr sigue siendo modificable. APUNTADORES Y CADENA – LECCIÓN 20 20-13 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: MODIFICAR2.CPP, intenta modificar un apuntador constante hacia datos no constantes. */ #include <iostream.h> //Para cout y cin void main(void) { int x, y; /* ptr es un apuntador constante hacia un entero. Se puede modificar un entero a través de ptr, pero ptr siempre apunta a la misma localidad de memoria. */ int *const ptr = &x; *ptr = 7; ptr = &y; }//Fin de main() Los mensajes que envía el compilador son los siguientes: Cannot modify a const object ‘y’ is declared but never used El menor privilegio de acceso se otorga mediante un apuntador constante hacia datos constantes. Tal apuntador siempre apunta a la misma localidad de memoria y los datos de dicha localidad no pueden ser modificados. Esta es la manera en que debe pasarse un arreglo a una función que sólo consulta dicho arreglo por medio de notación de índices y que no lo modifica. Ejemplo 20.7 El programa MODIFICAR3.CPP, declara la variable de apuntador ptr como de tipo const int *const. Esta declaración se lee de derecha a izquierda como ptr es un apuntador constante hacia un entero constante. El programa muestra los mensajes de error generados al intentar modificar los datos hacia los que apunta ptr y al intentar alterar la dirección almacenada en la variable de apuntador. Observe que al intentar enviar a la salida el valor al que apunta ptr no se genera un error, pues en la instrucción de salida no se está modificando nada. /* El siguiente programa: MODIFICAR3.CPP, intenta modificar un apuntador constante hacia datos constantes. */ #include <iostream.h> //Para cout y cin void main(void) { int x = 5, y; / /* ptr es un apuntador constante hacia un entero constante. ptr siempre apunta a la misma localidad y el entero de dicha localidad no puede ser modificado. */ const int *const ptr = &x; cout << *ptr << endl; *ptr = 7; ptr = &y; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-14 MIGUEL Á. TOLEDO MARTÍNEZ ORDENAMIENTO DE burbuja MEDIANTE LLAMADA POR REFERENCIA Ejemplo 20.8 El programa BURBUJA1.CPP, de la lección 18 se modificará para que utilice dos funciones clasificBurbuja() e intercambiar() (véase el programa BURBUJA.CPP) La función clasificBurbuja() se encarga de ordenar el arreglo. Llama a la función intercambiar() para que intercambie los elementos del arreglo arreglo[j] y arreglo[j + 1] Recuerde que C++ aplica el ocultamiento de información entre funciones, por lo que intercambiar() no tiene acceso a los elementos individuales del arreglo clasificBurbuja() Debido a que clasificBurbuja() quiere que intercambiar() tenga acceso a los elementos del arreglo que se intercambiarán, pasa cada uno de ellos a intercambiar() mediante una llamada por referencia; la dirección de cada elemento del arreglo se pasa explícitamente. Aunque los arreglos completos se pasan automáticamente mediante llamada por referencia, los elementos individuales del arreglo son escalares y por lo general se pasan mediante una llamada por valor. Así que clasificBurbuja() utiliza el operador de dirección (&) en cada elemento del arreglo en la llamada de intercambiar(), como sigue: Intercambiar(&arreglo[j], &arreglo[j + 1]); Poniendo en práctica la llamada por referencia. La función intercambiar() recibe &arreglo[j] en la variable de apuntador elemento1Ptr. Gracias al ocultamiento de información, intercambiar() no puede saber el nombre arreglo[j], pero puede utilizar *elemento1Ptr como sinónimo de arreglo[j] Por lo tanto, cuando intercambiar() hace referencia a *elemento1Ptr, de hecho está referenciando a arreglo[j] en clasificBurbuja() De igual manera, cuando intercambiar() hace referencia a *elemento2Ptr, en realidad está referenciando a arreglo[j + 1] en clasificBurbuja() Aun cuando no se permite que intercambiar() diga: temporal = arreglo[j]; arreglo[j] = arreglo[j + 1]; arreglo[j + 1] = temporal; se logra exactamente el mismo efecto mediante: en la función intercambiar del programa BURBUJA.CPP /* El siguiente programa: BURBUJA.CPP, pone los valores en un arreglo, los ordena en orden ascendente e imprime el arreglo resultante. */ #include <iostream.h> #include <iomanip.h> //Para cout y cin //Para setw() void clasificBurbuja(int *, const int); void main(void) { const int TAMANO_ARREGLO = 10; int arreglo[TAMANO_ARREGLO] = {2, 6, 4, 8, 10, 12, 89, 68, 45, 37}; int i; cout << "Datos en el orden original\n"; for(i = 0; i < TAMANO_ARREGLO; i++) cout << setw(4) << arreglo[i]; clasificBurbuja(arreglo, TAMANO_ARREGLO); //Ordena el arreglo cout << "\nDatos en orden ascendente\n"; APUNTADORES Y CADENA – LECCIÓN 20 20-15 MIGUEL Á. TOLEDO MARTÍNEZ for(i = 0; i < TAMANO_ARREGLO; i++) cout << setw(4) << arreglo[i]; cout << endl; }//Fin de main() void clasificBurbuja(int *arre, const int tamano) { void intercambiar(int *, int *); for(int pasada = 0; pasada < tamano - 1; pasada++) for(int j = 0; j < tamano -1; j++) if(arre[j] > arre[j+1]) intercambiar(&arre[j], &arre[j+1]); }//Fin de clasificBurbuja() void intercambiar(int *elemento1Ptr, int *elemento2Ptr) { int temporal = *elemento1Ptr; *elemento1Ptr = *elemento2Ptr; *elemento2Ptr = temporal; }//Fin de intercambiar() Deben observarse varias características de la función clasificBurbuja() El encabezado de la función declara arreglo como int *arre, en lugar de como int arre[], indicando que clasificBurbuja() recibe como argumento un arreglo de un solo índice (nuevamente, estas notaciones son intercambiables) El parámetro tamano se declara como const, aplicando el principio de menor privilegio. Aunque el parámetro tamano recibe una copia de un valor de main() y la modificación de la copia no puede cambiar el valor en main() clasificBurbuja() no necesita alterar tamano para que cumpla su tarea. El tamaño del arreglo permanece fijo durante la ejecución de clasificBurbuja() Por lo tanto, tamano se declara como const para asegurarse de que no se pueda modificar. Si el tamaño del arreglo se modificara durante el proceso de ordenamiento, el algoritmo de ordenamiento no se ejecutaría correctamente. El prototipo de la función intercambiar() está incluido en el cuerpo de la función clasificBurbuja() porque es la única función que llama a intercambiar() La colocación del prototipo en clasificBurbuja() restringe las llamadas correctas a intercambiar() a aquellas que se llevan a cabo desde clasificBurbuja() Otras funciones que intentan llamar a intercambiar() no tienen acceso a un prototipo de función apropiado. Por lo general, éste es un error de sintaxis, pues C++ requiere prototipos de función. Observe que la función clasificBurbuja() recibe como parámetro el tamaño del arreglo. Dicha función debe saber el tamaño del arreglo para poder ordenarlo. Cuando se pasa un arreglo a una función, ésta recibe la dirección de memoria del primer elemento del arreglo. El tamaño del arreglo se debe pasar por separado. Al definir la función clasificBurbuja() para que reciba como parámetro el tamaño del arreglo, se logra que cualquier programa que ordene arreglos de enteros de un solo índice de tamaño arbitrario utilice dicha función. El tamaño del arreglo podría haberse programado directamente en la función. Esto limita el uso de la función a un arreglo de un tamaño específico y reduce su capacidad de reutilización. Sólo los programas que procesen arreglos de enteros de un solo índice y que sean del tamaño especificado podrán utilizar la función. Ejemplo 20.9 C++ ofrece el operador unario sizeof, el cual determina el tamaño en bytes de un arreglo (o de cualquier otro tipo de datos) durante la compilación del programa. Cuando al nombre de un arreglo se le aplica el operador sizeof, como en el programa SIZEOF.CPP, éste devuelve la cantidad total de bytes que hay en el APUNTADORES Y CADENA – LECCIÓN 20 20-16 MIGUEL Á. TOLEDO MARTÍNEZ arreglo como un valor de tipo size_t, que generalmente es un unsigned int. La computadora empleada aquí almacena las variables de tipo float en 4 bytes de memoria, y arreglo se ha declarado como de 20 elementos, por lo que arreglo ocupa 80 bytes de memoria. Al aplicarle el operador sizeof a un parámetro de apuntador de una función que recibe un arreglo como argumento, dicho operador devuelve el tamaño en bytes (4) del apuntador, no el tamaño del arreglo. /* El siguiente programa: SIZEOF.CPP, utiliza el operador sizeof() aplicado a un nombre de arreglo, devuelve la cantidad de bytes del arreglo. */ #include <iostream.h> //Para cout y cin size_t obtenerTamano(float *); void main(void) { float arreglo[20]; cout << "El número de bytes en el arreglo es " << sizeof(arreglo) << "\nEl número de bytes devuelto por obtenerTamano() es " << obtenerTamano(arreglo) << endl; }//Fin de main() size_t obtenerTamano(float *ptr) { return sizeof(ptr); }//fin de obtenerTamano() El número de elementos de un arreglo también puede determinarse por medio de los resultados de dos operaciones de sizeof. Por ejemplo, considere la siguiente declaración de arreglo: double arregloReal[22]; Si las variables del tipo de datos double se almacenan en 8 bytes de memoria, el arreglo arregloReal contiene un total de 176 bytes. Para determinar el número de elementos del arreglo, puede utilizarse la siguiente expresión: sizeof (arregloReal) / sizeof(double); La expresión determina el número de bytes en el arreglo arregloReal y divide este valor entre el número de bytes que se utiliza en memoria para almacenar un valor double. Ejemplo 20.10 El programa SIZEOF2.CPP se vale del operador sizeof para calcular el número de bytes utilizados para almacenar cada uno de los tipos de datos estándar de la computadora particular que se está empleado. /* El siguiente programa: SIZEOF2.CPP, muestra el uso del operador sizeof */ #include <iostream.h> #include <iomanip.h> APUNTADORES Y CADENA – LECCIÓN 20 //Para cout y cin //Para setw() 20-17 MIGUEL Á. TOLEDO MARTÍNEZ void main(void) { char c; short s; int i; long l; float f; double d; long double ld; int arreglo[20], *ptr = arreglo; cout << "sizeof c = " << sizeof c << "\tsizeof(char) = " << sizeof(char) << "\nsizeof s = " << sizeof s << "\tsizeof(short) = " << sizeof(short) << "\nsizeof i = " << sizeof i << "\tsizeof(int) = " << sizeof(int) << "\nsizeof l = " << sizeof l << "\tsizeof(long) = " << sizeof(long) << "\nsizeof f = " << sizeof f << "\tsizeof(float) = " << sizeof(float) << "\nsizeof d = " << sizeof d << "\tsizeof(double) = " << sizeof(double) << "\nsizeof ld = " << sizeof ld << "\tsizeof(long double) = " << sizeof(long double) << "\nsizeof arreglo = " << sizeof arreglo << "\nsizeof ptr = " << sizeof ptr << endl; }//Fin de main() El operador sizeof se puede aplicar a cualquier nombre de variable, nombre de tipo o valor constante. Cuando se aplica a un nombre de variable (que no sea un nombre de arreglo) o valor constante, se devuelve el número de bytes que se emplean para almacenar el tipo de variable o constante específicos. Observe que los paréntesis que se utilizan con sizeof son obligatorios si se indica como operando un nombre de tipo; no se requieren si se indica como operando un nombre de variable. Es importante recordar que sizeof es un operador, no una función. EXPRESIONES DE APUNTADORES Y ARITMÉTICA DE APUNTADORES Los apuntadores son operandos válidos en las expresiones aritméticas, las expresiones de asignación y las expresiones de comparación. Sin embargo, no todos los operadores que se utilizan normalmente en estas expresiones son válidos con variables de apuntador. Esta sección describe los operadores que pueden tener apuntadores como sus operandos, además de la manera en que se utilizan. Es posible llevar a cabo un conjunto limitado de operaciones aritméticas sobre los apuntadores. Un apuntador puede incrementarse (++) o decrementarse (--), además es posible sumarle un entero a un apuntador (+ o +=), restar un entero de un apuntador (- o -=) y restar un apuntador de otro. Suponga que se ha declarado un arreglo int v[5] y que su primer elemento está en la localidad 3000 de la memoria. Además, suponga que se ha inicializado el apuntador vPtr para que apunte a v[0], es decir, el valor de vPtr es 3000. La figura 20.5 es un diagrama de dicha APUNTADORES Y CADENA – LECCIÓN 20 20-18 MIGUEL Á. TOLEDO MARTÍNEZ situación en una máquina con enteros de 4 bytes. Observe que vPtr puede inicializarse para que apunte al arreglo v mediante cualquiera de las siguientes instrucciones: vPtr = v; vPtr = &v[0]; Localidad 3000 3004 v[0] 3008 v[1] 3012 v[2] 3016 v[3] v[4] • Variable de apuntador vPtr Figura 20.5. El arreglo v y la variable de apuntador vPtr que apunta a v. En la aritmética convencional, la suma de 3000 + 2 da el valor 3002. Normalmente éste no es el caso con la aritmética de apuntadores. Cuando a un apuntador se le suma o resta un entero, dicho apuntador no se incrementa o decrementa en la misma cantidad que el entero, sino en el entero por el tamaño del objeto hacia el que apunta el apuntador. El número de bytes depende del tipo de datos del objeto. Por ejemplo, la instrucción vPtr += 2; producirá 3008 (3000 + 2 * 4), suponiendo que los enteros se almacenan en 4 bytes de memoria. En el arreglo v, vPtr ahora apuntará a v[2] (vea la figura 20.6) Localidad 3000 3004 v[0] v[1] 3008 v[2] 3012 v[3] 3016 v[4] • Variable de apuntador vPtr Figura 20.6. El apuntador vPtr después de la aritmética de apuntadores. Si se almacena un entero en 2 bytes de memoria, entonces el cálculo anterior dará la localidad de memoria 3004 (3000 + 2 * 2) Si el arreglo fuera de un tipo de datos distinto, la instrucción previa incrementaría el apuntador por el doble del número de bytes necesarios para almacenar un objeto de dicho tipo de datos. Al efectuar aritmética de apuntadores sobre un APUNTADORES Y CADENA – LECCIÓN 20 20-19 MIGUEL Á. TOLEDO MARTÍNEZ arreglo de caracteres, los resultados serán consistentes con la aritmética común, pues cada carácter tiene un byte de longitud. Si vPtr se hubiera incrementado a 3016, que apunta a v[4], la instrucción vPtr -= 4; establecería vPtr nuevamente a 3000 –el comienzo del arreglo. Si se está incrementando o decrementando en uno un apuntador, es posible utilizar los operadores de incremento (++) y decremento (--) Las instrucciones ++vPtr; vPtr++; incrementan el apuntador para que apunte a la siguiente localidad del arreglo. Las instrucciones --vPtr; vPtr--; decrementan el apuntador para que apunte al elemento previo del arreglo. Las variables de apuntador pueden restarse entre ellas. Por ejemplo, si vPtr contiene la localidad 3000 y v2Ptr la dirección 3008, la instrucción x = v2Ptr –vPtr; le asignará a x el número de elementos del arreglo de vPtr a v2Ptr, que es 2 en este caso. La aritmética de apuntadores carece de significado a menos que se lleve a cabo sobre un arreglo. No podemos suponer que dos variables del mismo tipo están almacenadas de manera contigua en la memoria, a menos que sean elementos adyacentes de un arreglo. Es posible asignar un apuntador a otro, si ambos son del mismo tipo. De otro modo, es necesario emplear un operador de conversión mediante cast para convertir el valor del apuntador del lado derecho de la asignación al tipo de apuntador a la izquierda de la asignación. La excepción a esta regla es el apuntador a void (es decir, void *), que es un apuntador genérico capaz de representar cualquier tipo de apuntador. Todos los tipos de apuntadores pueden ser asignados a void sin necesidad de una conversión mediante cast. Sin embargo, un apuntador a void no puede asignarse directamente a un apuntador de otro tipo; primero hay que convertir el apuntador void al tipo de apuntador adecuado. No es posible desreferenciar un apuntador void *. Por ejemplo, el compilador sabe que un apuntador a int hace referencia a cuatro bytes de memoria en una máquina con enteros de 4 bytes, pero los apuntadores a void simplemente contienen localidades de memoria para un tipo de datos desconocido; por lo tanto, el compilador no sabe el número preciso de bytes a los que hace referencia el apuntador. El compilador debe saber el tipo de datos para determinar el número de bytes a desreferenciar para un apuntador en particular. En el caso de un apuntador a void, esta cantidad de bytes no puede determinarse a partir del tipo. APUNTADORES Y CADENA – LECCIÓN 20 20-20 MIGUEL Á. TOLEDO MARTÍNEZ Los apuntadores se pueden comparar por medio de operadores de igualdad y relacionales, pero tales comparaciones no tienen significado a menos que los apuntadores apunten a miembros del mismo arreglo. Las comparaciones de apuntadores comparan las direcciones almacenadas en los apuntadores. Una comparación de dos apuntadores que apuntan al mismo arreglo podría mostrar, por ejemplo, que un apuntador apunta a un elemento de índice mayor que el otro apuntador. Un uso común de la comparación de apuntadores es la determinación de si un apuntador es 0. RELACIÓN ENTRE APUNTADORES Y ARREGLOS Los arreglos y los apuntadores están relacionados íntimamente en C++ y se pueden utilizar de manera casi intercambiable. Es posible pensar en los nombres de arreglos como si fueran apuntadores constantes. Los apuntadores pueden servir para realizar cualquier operación en la que intervengan índices de arreglos. Suponga que se han declarado tanto el arreglo de entero b[5] como la variable entera de apuntador bPtr. Debido a que el nombre del arreglo (sin índice) es un apuntador al primer elemento del mismo, podemos establecer bPtr a la dirección del primer elemento del arreglo b por medio de la instrucción bPtr = b; Esto es equivalente a tomar la dirección del primer elemento del arreglo, como sigue bPtr = &b[0]; El elemento b[3] del arreglo se puede referencia de manera alterna con la expresión de apuntador *(bPtr + 3) El 3 de la expresión previa es el desplazamiento al apuntador. Cuando el apuntador apunta al inicio de un arreglo, el desplazamiento indica el elemento del arreglo al que se hará referencia y el valor de dicho desplazamiento es idéntico al índice del arreglo. La notación previa se conoce como notación de apuntador y desplazamiento. Los paréntesis son necesarios porque la precedencia de * es mayor que la de +. Sin éstos, la expresión anterior sumaría 3 al valor de la expresión *bPtr (es decir, se sumaría 3 a b[0]), suponiendo que bPtr apuntara al inicio del arreglo. Así como el elemento del arreglo se puede referenciar con una expresión de apuntador, la dirección &b[3] se puede escribir con la expresión de apuntador bPtr + 3 El arreglo mismo puede ser tratado como apuntador y utilizado con aritmética de apuntadores. Por ejemplo, la expresión *(b + 3) APUNTADORES Y CADENA – LECCIÓN 20 20-21 MIGUEL Á. TOLEDO MARTÍNEZ también se refiere al elemento del arreglo b[3]. En general, todas las expresiones de arreglos con índices se pueden escribir con un apuntador y un desplazamiento. En este caso, se utilizó notación de apuntador y desplazamiento con el nombre del arreglo como apuntador. Observe que la instrucción previa no modifica de ninguna manera el nombre del arreglo; b aún apunta al primer elemento de la misma. Los puntadores se pueden indexar de la misma manera que los arreglos. Por ejemplo, la expresión bPtr[1] hace referencia al elemento b[1] del arreglo; esta expresión se conoce como notación de apuntador e índice. Recuerde que, en esencia, los nombres de arreglos son apuntadores constantes; siempre apuntan al inicio del arreglo. Por lo tanto, la expresión b += 3 es inválida debido a que intenta modificar el valor del nombre del arreglo por medio de aritmética de apuntadores. Ejemplo 20.11 El programa APUNTADORES.CPP, utiliza los cuatro métodos que hemos explicado para hacer referencia a los elementos de un arreglo (indización de arreglos, apuntador y desplazamiento con el nombre del arreglo como apuntador, indización de apuntadores y apuntador y desplazamiento con un apuntador) para imprimir los cuatro elementos del arreglo de enteros b. /* El siguiente programa: APUNTADORES.CPP, utiliza las notaciones de índices y por apuntadores con arreglos. */ #include <iostream.h> //Para cout y cin void main(void) { int b[] = {10, 20, 30, 40}; int *bPtr = b; //Establece bPtr para que apunte al arreglo b cout << "Arreglo b impreso con:\n" << "Notación de índices\n"; for(int i = 0; i < 4; i++) cout << "b[" << i << "] = " << b[i] << '\n'; cout << "\nNotación apuntador/desplazamiento en donde\n" << "el apuntador es el nombre del arreglo\n"; for(int desplazamiento = 0; desplazamiento < 4; desplazamiento++) cout << "*(b + " << desplazamiento << ") = " << *(b + desplazamiento) << '\n'; APUNTADORES Y CADENA – LECCIÓN 20 20-22 MIGUEL Á. TOLEDO MARTÍNEZ cout << "\nNotacion de índices de apuntador\n"; for(int i = 0; i < 4; i++) cout << "bPtr[" << i << "] = " << bPtr[i] << '\n'; cout << "\nNotación apuntador/desplazamiento\n"; for(int desplazamiento = 0; desplazamiento < 4; desplazamiento++) cout << "*(bPtr + " << desplazamiento << ") = " << *(bPtr + desplazamiento) << '\n'; }//Fin de main() Ejemplo 20.12 Para ilustrar aún más que los arreglos y los apuntadores son intercambiables, vea las dos funciones de copia de cadenas (copia1() y copia2()) del programa APUNTADORES2.CPP. Ambas funciones copian una cadena a un arreglo de caracteres. Tras una comparación de los prototipos de función de copia1() y copia2(), ambas funciones parecen idénticas (gracias a que los arreglos y los apuntadores son intercambiables) Estas funciones llevan a cabo la misma tarea, pero se implementan de manera diferente. /* El siguiente programa: APUNTADORES2.CPP, copia una cadena por medio de notación de arreglos y notación de apuntadores. */ #include <iostream.h> //Para cout y cin void copia1(char *, const char *); void copia2(char *, const char *); void main(void) { char cadena1[10], *cadena2 = "Hola", cadena3[10], cadena4[] = "Adiós"; copia1(cadena1, cadena2); cout << "cadena1 = " << cadena1 << endl; copia2(cadena3, cadena4); cout << "cadena3 = " << cadena3 << endl; }//Fin de main() //Copia s2 a s1 mediante notación de arreglos void copia1(char *s1, const char *s2) { for(int i = 0; (s1[i] = s2[i]) != '\0'; i++) ; //El cuerpo no hace nada }//Fin de copia1() //Copia s2 a s1 mediante notación de apuntadores void copia2(char *s1, const char *s2) { for(; (*s1 = *s2) != '\0'; s1++, s2++) ; //El cuerpo no hace nada }//Fin de copia2() APUNTADORES Y CADENA – LECCIÓN 20 20-23 MIGUEL Á. TOLEDO MARTÍNEZ La función copia1() utiliza notación de índices de arreglo para copiar la cadena s2 al arreglo de caracteres s1. Además, declara una variable entera i, que funciona como contador, la cual será el índice del arreglo. El encabezado de la estructura for realiza la operación completa de copia –su cuerpo es la instrucción vacía. El encabezado especifica que i se inicializa a cero y se incrementa en uno con cada iteración del ciclo. La condición for, (s1[i] = s2[i]) != ‘\0’, realiza la operación de copia, carácter por carácter, de s2 a s1. Al llegar al carácter nulo en s2, éste es asignado a s1 y termina el ciclo, pues el carácter nulo es igual a ‘\0’. Recuerde que el valor de una instrucción de asignación el valor asignado al argumento de la izquierda. La función copia2() se vale de apuntadores y aritmética de apuntadores para copiar la cadena de s2 al arreglo de caracteres s1. Otra vez, el encabezado de la estructura for efectúa toda la operación de copia. Dicho encabezado no incluye ninguna iniciación de variables. Como en la función copia1(), la condición (*s1 = *s2) != ‘\0’ realiza la operación de copia. El apuntador s2 es desreferenciado y el carácter resultante es asignado al apuntador desreferenciado s1. Tras la asignación que sucede en la condición, se incrementan los apuntadores para que apunten al siguiente elemento del arreglo s1 y al siguiente carácter de la cadena s2, respectivamente. Al encontrar el carácter nulo en s2, se le asigna al apuntador desreferenciado s1 y termina el ciclo. Observe que el primer argumento, tanto de copia1() como de copia2(), debe ser un arreglo lo bastante grande para contener la cadena del segundo argumento. De otra manera, puede suceder un error cuando se intente escribir en una localidad de memoria fuera de los límites del arreglo. Además observe que el segundo parámetro de cada función se declara como const char * (cadena constante) En ambas funciones se copia el segundo argumento al primero –los caracteres se copian del segundo argumento, uno a la vez, pero nunca se modifican. Por lo tanto, se declara el segundo parámetro de modo que apunte a un valor constante, aplicando así el principio de menor privilegio. Ninguna de las dos funciones necesita modificar el segundo argumento, por lo tanto no tienen la capacidad de hacerlo. ARREGLOS DE APUNTADORES Los arreglos pueden contener apuntadores. Un uso común de tal estructura de datos es para formar arreglos de cadenas. Cada entrada del arreglo es una cadena, pero en C++ una cadena es, en esencia, un apuntador a su primer carácter. Por lo tanto, cada entrada de un arreglo de cadenas es, de hecho, un apuntador al primer carácter de una cadena. Considere la declaración del arreglo de cadena palos, que podría ser útil para representar una baraja de naipes. char *palos[4] = {“Corazones”, “Diamantes”, “Tréboles”, “Espadas”}; La parte palos[4] de la declaración indica un arreglo de 4 elementos. La parte char * indica que cada elemento del arreglo palos es del tipo apuntador a char. Los cuatro valores a poner en el arreglo son los palos de la baraja, “Corazones”, “Diamantes”, “Tréboles” y “Espadas”. Cada uno de ellos se almacena en memoria como una cadena de caracteres que termina con el carácter nulo y cuya longitud es de un carácter más que el número de caracteres que hay entre comillas. Las cadenas son de 10, 10, 9 y 8 caracteres de longitud, respectivamente. Aunque parece que estas cadenas se ponen en el arreglo palos, de hecho sólo se almacenan apuntadores (vea la figura 20.7)-cada apuntador apunta al primer carácter de su cadena correspondiente. Por lo tanto, aun cuando el arreglo palos es de tamaño fijo, da acceso a cadenas de caracteres de cualquier longitud. Esta flexibilidad es un ejemplo de las poderosas posibilidades de estructuración de datos que tiene C++. Las cadenas de los distintos palos de la baraja podrían ponerse en un arreglo de doble índice en el que cada fila representa un palo y cada columna una de las letras del nombre del palo. Tal estructura de datos debe tener un número fijo de columnas por fila, que tiene que ser tan grande como la cadena más larga. Por lo tanto, se desperdiciaría una cantidad importante de memoria si se almacenara un gran número de cadenas cuando la mayoría de ellas fuera más corta APUNTADORES Y CADENA – LECCIÓN 20 20-24 MIGUEL Á. TOLEDO MARTÍNEZ que la cadena más larga. Para ayudar a representar una baraja, en la siguiente sección se emplean arreglos de cadenas. palos[0] • ‘C’ ‘o’ ‘r’ ‘a’ ‘z’ ‘o’ ‘n’ ‘e’ ‘s’ ‘\0’ palos[0] • ‘D’ ‘i’ ‘a’ ‘m’ ‘a’ ‘n’ ‘t’ ‘e’ ‘s’ ‘\0’ palos[0] • ‘T’ ‘r’ ‘e’ ‘b’ ‘o’ ‘l’ ‘e’ ‘s’ ‘\0’ palos[0] • ‘E’ ‘s’ ‘p’ ‘a’ ‘d’ ‘a’ ‘s’ ‘\0’ Figura 20.7. Representación gráfica del arreglo palos. CASO DE ESTUDIO: Simulación de barajado y repartición de naipes En esta sección utilizamos la generación aleatoria de números para desarrollar un programa de simulación de barajado y repartición de naipes. Este programa puede utilizarse después para implementar programas de distintos juegos de baraja. Para relevar algunos sutiles problemas de desempeño, hemos utilizado intencionalmente algoritmos de barajado y repartición subóptimos. En los ejercicios desarrollaremos algoritmos más eficientes. Mediante el enfoque de refinación descendente paso a paso, se desarrolla un programa que baraja 52 naipes y luego los reparte. Dicho enfoque es especialmente útil cuando se abordan problemas mayores y más complejos de los que hemos visto en las primeras lecciones. Para representar la baraja, utilizamos un arreglo paquete de doble índice, de 4 por 13 (vea la figura 20.8) Las filas corresponden a los palos: la fila 0 a los corazones, la fila 1 a los diamantes, la 2 a los tréboles y la 3 a las espadas. Las columnas corresponden a los valores de los naipes: las columnas 0 a 9 corresponden del as al 10, respectivamente, y las columnas de la 10 a la 12 corresponden a la sota, la reina y el rey. Cargaremos el arreglo de cadenas palos con las cadenas de caracteres que representan los cuatro palos y el arreglo de cadena cara con las cadenas de caracteres que corresponden a los trece valores de los naipes. 0 1 2 3 4 5 6 7 8 9 10 11 12 Corazones 0 Diamantes 1 Tréboles 2 Espadas 3 Figura 20.8. Arreglo (paquete) de doble índice que representa una baraja. APUNTADORES Y CADENA – LECCIÓN 20 20-25 MIGUEL Á. TOLEDO MARTÍNEZ En la figura 20.8 cada columna (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12) representan: As, Dos, Tres, Cuatro, Cinco, Seis, Siete, Ocho, Nueve, Diez, Sota, Reina y Rey respectivamente. Así paquete[2][12] (parte sombreada de la figura 20.8) representa al rey de tréboles. Tréboles Rey Esta baraja simulada puede barajarse como sigue. Primero, el arreglo paquete se pone en ceros. Luego se seleccionan al azar una fila (renglón, 0 a 3) y una columna (columna, 0 a 12) El número 1 se inserta en el elemento del arreglo paquete[renglón][columna], indicando que este naipe va a ser el primero de la baraja en ser repartido. Este proceso continua con la inserción aleatoria de los números 2, 3, ..., 52 en el arreglo paquete, con lo que se indica el segundo, tercero, ... y quincuagésimo segundo naipe a repartir. A medida que se va llenando el arreglo paquete con números de naipe, es posible que una carta se seleccione dos veces, es decir, que paquete[renglón][columna] sea distinto de cero cuando sea seleccionado. Esta selección simplemente se ignora y se seleccionan al azar otro renglón y otra columna hasta dar con un naipe no seleccionado. Tarde o temprano los números del 1 al 52 ocuparán los 52 espacios del arreglo paquete. En este momento se termina el barajado de los naipes. Este algoritmo de barajado podría ejecutarse durante un tiempo indefinidamente grande si se seleccionan repetidamente al azar naipes que ya han sido barajados. Este fenómeno se conoce como aplazamiento indefinido. En los ejercicios se estudiará un mejor algoritmo de barajado que elimina la posibilidad de dicho fenómeno. Para repartir el primer naipe se busca en el arreglo la posición paquete[renglón][columna] que sea igual a 1. Esto se logra mediante una estructura for anidada que varía renglón de 0 a 3 y columna de 0 a 12. ¿A qué naipe corresponde dicho espacio del arreglo? El arreglo palos ha sido precargado con los cuatro palos, por lo que, para obtener el palo, se imprime la cadena de caracteres palos[renglón] De igual manera, para obtener el valor del naipe, se imprime la cadena de caracteres cara[columna] También se imprime la cadena “ de “. La impresión de esta información en el orden correcto permite imprimir cada naipe en la forma Rey de Tréboles, As de Diamantes, etcétera. Procedamos mediante la refinación descendente paso a paso. La parte superior simplemente es: Baraja y reparte 52 naipes. La primera refinación da: Inicializa el arreglo de palos (palos) Inicializa el arreglo de valores (cara) Inicializa el arreglo de la baraja (paquete) Baraja los naipes. Reparte los 52 naipes. Baraja los naipes se puede expandir como sigue: Por cada uno de los 52 naipes Coloca el número del naipe en un espacio desocupado de la baraja seleccionada al azar. APUNTADORES Y CADENA – LECCIÓN 20 20-26 MIGUEL Á. TOLEDO MARTÍNEZ Reparte los 52 naipes se puede expandir como sigue: Por cada uno de los 52 naipes Encuentra un número de naipe en el arreglo de la baraja e imprime su valor y palo. La incorporación de estas expansiones da la segunda refinación completa: Inicializa el arreglo de palos. Inicializa el arreglo de valores. Inicializa el arreglo de la baraja. Por cada uno de los 52 naipes Coloca el número del naipe en un espacio desocupado de la baraja seleccionando al azar. Por cada uno de los 52 naipes Encuentra un número de naipe en el arreglo de la baraja e imprime su valor y palo. Coloca el número del naipe en un espacio desocupado de la baraja seleccionado al azar se puede expandir como sigue: Selecciona al azar el espacio de la baraja. Mientras ya haya sido seleccionado el espacio de la baraja Selecciona al azar dicho espacio. Coloca el número del naipe en el espacio seleccionado de la baraja. Encuentra el número de naipe en el arreglo de la baraja e imprime su valor y palo se puede expandir como sigue: Por cada espacio del arreglo de la baraja Si el espacio contiene un número de naipe Imprime el valor y el palo del naipe. La incorporación de estas expansiones da la tercera refinación: Inicializa el arreglo de palos. Inicializa el arreglo de valores. Inicializa el arreglo de la baraja. Por cada uno de los 52 naipes Selecciona al azar dicho espacio. Coloca el número de naipe en el espacio seleccionado de la baraja. Por cada uno de los 52 naipes Por cada espacio del arreglo de la baraja Si el espacio contiene un número de naipe Imprime el valor y el palo del naipe. Con esto se termina el proceso de refinación. Observe que este programa es más eficiente si las partes de barajado y repartición del algoritmo se combinan, de modo que cada naipe se reparta a medida que se pone en la baraja. Decidimos programar estas operaciones por separado debido a que los naipes normalmente se reparten después de haber sido barajados (no a medida que se barajan) APUNTADORES Y CADENA – LECCIÓN 20 20-27 MIGUEL Á. TOLEDO MARTÍNEZ Ejemplo 20.13 En el programa BARAJAS.CPP, se muestra el barajado y repartición de naipes. Observe el formato de salida que se utiliza en la función repartir(): cout << setw(5) << setiosflags(ios::right) << wCara[columna] << “ de “ << setw(8) << setiosflags(ios::left) << wPalos[renglon] << (baraja % 2 == 0 ? ‘\n’ : ‘\t’); La instrucción de salida anterior provoca que el valor del naipe se envíe a la salida justificado a la derecha en un campo de 5 caracteres, y que el palo se envíe a la salida justificado a la izquierda en un campo de 8 caracteres. La salida se imprime en formado de dos columnas. Si el naipe que se envía a la salida está en la primera columna, se envía una tabulación a la salida tras él para saltar a la segunda columna: de otra manera, se envía un salto de línea. /* El siguiente programa: BARAJAS.CPP, baraja y reparte naipes. */ #include <iostream.h> #include <iomanip.h> #include <stdlib.h> #include <time.h> //Para cout y cin //Para setw() //Para srand() y rand() //Para Time() void barajar(int[][13]); void repartir(const int [][13], const char *[], const char *[]); void main(void) { const char *palos[4] = {"Corazones", "Diamantes", "Tréboles", "Espadas"}; const char *cara[13] = {"As", "Dos", "Tres", "Cuatro", "Cinco", "Seis", "Siete", "Ocho", "Nueve", "Diez", "Sota", "Reina", "Rey"}; int paquete[4][13] = {0}; srand(time(0)); barajar(paquete); repartir(paquete, cara, palos); }//Fin de main() void barajar(int wPaquete[][13]) { int renglon, columna; for(int baraja = 1; baraja <= 52; baraja++) { do { renglon = rand() % 4; columna = rand() % 13; } while(wPaquete[renglon][columna] != 0); wPaquete[renglon][columna] = baraja; }//Fin del for }//Fin de barajar() APUNTADORES Y CADENA – LECCIÓN 20 20-28 MIGUEL Á. TOLEDO MARTÍNEZ void repartir(const int wPaquete[][13], const char *wCara[], const char *wPalos[]) { for(int baraja = 1; baraja <= 52; baraja++) for(int renglon = 0; renglon <= 3; renglon++) for(int columna = 0; columna <= 12; columna++) if(wPaquete[renglon][columna] == baraja) cout << setw(5) << setiosflags(ios::right) << wCara[columna] << " de " << setw(8) << setiosflags(ios::left) << wPalos[renglon] << (baraja % 2 == 0 ? '\n' : '\t'); }//Fin de repartir() Hay una debilidad en el algoritmo de repartición. Una vez encontrado el número que se busca, incluso si se encuentra al primer intento, las estructuras for internas continúan buscando dicho número en los elementos restantes de paquete. En los ejercicios se corrige esta deficiencia. APUNTADORES Y FUNCIONES Un apuntador a una función contiene la dirección que tiene la función en la memoria. Según hemos visto el nombre de un arreglo es, en realidad, la dirección inicial en memoria del primer elemento del mismo. Del mismo modo, un nombre de función en realidad es la dirección inicial en memoria del código que lleva a cabo la tarea de la función. Los apuntadores a funciones pueden ser pasados a las funciones, devueltos de ellas, almacenados en arreglos y asignados a otros apuntadores a funciones. Ejemplo 20.14 Para ilustrar el uso de los apuntadores a funciones, hemos modificado el programa de ordenamiento de burbuja de esta lección al cual llamamos BURBUJA.CPP, dando lugar al siguiente programa, BURBUJA2.CPP. Este nuevo programa consiste en main() y en las funciones burbuja(), intercambiar(), ascendente() y descendente() La función burbuja() recibe como argumento un apuntador a una función – sea ascendente() o descendente()- además de un arreglo de enteros y su tamaño. El programa le pide al usuario que seleccione si el arreglo debe ordenarse de manera ascendente o descendente. Si el usuario indica 1, a burbuja() se le pasa un apuntador a la función ascendente(), lo que causa que el arreglo se ordena de menor a mayor. Si el usuario indica 2, a burbuja() se le pasa un apuntador a la función descendente(), lo que causa que el arreglo se ordene de mayor a menor. /* El siguiente programa: BURBUJA2.CPP, de ordenamiento de usos múltiples que utiliza apuntadores a funciones. */ #include <iostream.h> #include <iomanip.h> //Para cout y cin //Para setw() void burbuja(int[], const int, int (*)(int, int)); int ascendente(int, int); int descendente(int, int); void main(void) { const int TAMANO_ARREGLO = 10; int orden, contador, arreglo[TAMANO_ARREGLO] APUNTADORES Y CADENA – LECCIÓN 20 = {2, 6, 4, 8, 10, 12, 89, 68, 45, 37}; 20-29 MIGUEL Á. TOLEDO MARTÍNEZ cout << "Teclee 1 para ordenar en forma ascendente,\n" << "Teclee 2 para ordenar en forma descendente: "; cin >> orden; cout << "\nDatos en el orden original\n"; for(contador = 0; contador < TAMANO_ARREGLO; contador++) cout << setw(4) << arreglo[contador]; if(orden == 1) { burbuja(arreglo, TAMANO_ARREGLO, ascendente); cout << "\nDatos en orden ascendente\n"; }//Fin del if else { burbuja(arreglo, TAMANO_ARREGLO, descendente); cout << "\nDatos en orden descendente\n"; }//Fin del else for(contador = 0; contador < TAMANO_ARREGLO; contador++) cout << setw(4) << arreglo[contador]; cout << endl; }//Fin de main() void burbuja(int trabajar[], const int tamano, int (*comparar)(int, int)) { void intercambiar(int *, int *); for(int pasada = 1; pasada < tamano; pasada++) for(int contador = 0; contador < tamano -1; contador++) if((*comparar)(trabajar[contador], trabajar[contador + 1])) intercambiar(&trabajar[contador], &trabajar[contador + 1]); }//Fin de burbuja void intercambiar(int *elemento1Ptr, int *elemento2Ptr) { int temporal; temporal *elemento1Ptr *elemento2Ptr }//Fin de intercambiar() int ascendente(int a, int b) { return b < a; }//Fin de ascendente() int descendente(int a, int b) { return b > a; }//Fin de descendente() APUNTADORES Y CADENA – LECCIÓN 20 = *elemento1Ptr; = *elemento2Ptr; = temporal; //Intercambia si b es menor que a //Intercambia si b es mayor que a 20-30 MIGUEL Á. TOLEDO MARTÍNEZ El siguiente parámetro aparece en el encabezado de la función burbuja(): int ( *comparar )( ínt, int) Esto le dice a burbuja() que espere un parámetro que es un apuntador a una función que recibe dos parámetros enteros y devuelve un resultado entero. Se necesitan paréntesis alrededor de *comparar debido a que * tiene menor precedencia que el paréntesis que encierra los parámetros de la función. Si no se hubieran incluido los paréntesis, la declaración habría sido int *comparar( int, int ) que declara una función que recibe dos enteros como parámetros y le devuelve un apuntador a un entero. El parámetro correspondiente del prototipo de función de burbuja() es Int (*) ( int, int) Observe que sólo se han incluido los tipos, pero, con fines de documentación, el programador puede incluir nombres que ignorará el compilador. La función que se le pasa a burbuja() es llamada en una instrucción if como sigue if((*comparar )( trabajar[ contador], trabajar[contador + 1])) Así como se desreferencia un apuntador a una variable para acceder al valor de ésta, se desreferencian los apuntadores a funciones para ejecutar dichas funciones. La llamada a la función podría haberse hecho sin desreferenciar el apuntador, como en if ( *comparar( trabajar[ contador ], trabajar[ contador + 1])) que utiliza el apuntador directamente como nombre de función. Preferirnos el primer método, donde la llamada a una función es a través de un apuntador, pues explícitamente ilustra que compare es un apuntador a una función que se desreferencia para llamar a la función. El segundo método para llamar a la función a través de un apuntador da la apariencia de que comparar() de hecho es una función. Esto puede confundir a algún usuario del programa que quisiera ver la definición de la función comparar() y descubriera que no se definió en ninguna parte del archivo. Uno de los usos de los apuntadores a funciones es en los sistemas operados por menús. Al usuario se le solicita desde un menú que dé una opción (por ejemplo, de 1 a 5) Cada opción es atendida por una función distinta. En un arreglo de apuntadores a funciones se almacenan los apuntadores a todas las funciones. Se toma la selección del usuario como índice del arreglo y se emplea el apuntador del mismo para llamar a la función. Ejemplo 20.15 El programa siguiente: APUNTADORES3.CPP presenta un ejemplo general de la mecánica de declaración y empleo de un arreglo de apuntadores a funciones. Se definen tres funciones (funcion1(), funcion2() y funcion3()) que toman un argumento entero y no devuelven nada. Los apuntadores a estas tres funciones se almacenan en el arreglo f, que se declara como sigue: void( *f[ 3] ) ( int) = { funcion1, funcion2, funcion3}; La declaración se lee comenzando por el par de paréntesis de la izquierda, f es un arreglo de 3 apuntadores a funciones que toman un int como argumento y devuelven void". El arreglo se inicializa con los nombres de las funciones (que, nuevamente, son apuntadores) Cuando el usuario introduce un valor entre 0 y 2, se toma como índice del arreglo de apuntadores a funciones. La llamada de la función se hace como sigue: APUNTADORES Y CADENA – LECCIÓN 20 20-31 MIGUEL Á. TOLEDO MARTÍNEZ ( *f[opcion]) (opcion); En la llamada, f[opcion] selecciona el apuntador que se encuentra en la localidad opcion del arreglo. El apuntador se desreferencia para que llame a la función, y opcion se pasa como argumento de la función. Cada función imprime el valor de su argumento y su nombre, indicando que se llamó de manera correcta. En los ejercicios se desarrollará un sistema operador por menús. /* El siguiente programa: APUNTADORES3.CPP, muestra un arreglo de apuntadores a funciones. */ #include <iostream.h> //Para cout y cin void funcion1(int); void funcion2(int); void funcion3(int); void main(void) { void(*f[3])(int) = {funcion1, funcion2, funcion3}; int opcion; cout << "Introduzca un número entre 0 y 2, 3 para terminar: "; cin >> opcion; while(opcion >= 0 && opcion < 3) { (*f[opcion])(opcion); cout << "Introduzca un número entre 0 y 2, 3 para terminar: "; cin >> opcion; }//Fin de while cout << "Ejecución del programa finalizada." << endl; }//Fin de main() void funcion1(int a) { cout << "Introdujo " << a << " por ello se llamó a funcion1()\n\n"; }//Fin de funcion1() void funcion2(int b) { cout << "Introdujo " << b << " por ello se llamó a funcion2()\n\n"; }//Fin de funcion2() void funcion3(int c) { cout << "Introdujo " << c << " por ello se llamó a funcion3()\n\n"; }//Fin de funcion3() APUNTADORES Y CADENA – LECCIÓN 20 20-32 MIGUEL Á. TOLEDO MARTÍNEZ INTRODUCCIÓN AL PROCESAMIENTO DE CARACTERES Y CADENAS En esta sección se presentarán algunas de las funciones de la biblioteca estándar que simplifican el procesamiento de cadenas. Las técnicas que se estudian aquí son adecuadas para el desarrollo de editores de texto, procesadores de texto, software de formación de páginas, sistemas tipográficos computarizados y otros tipos de software de procesamiento de texto. Aquí empleamos cadenas basadas en apuntadores. FUNDAMENTOS DE LOS CARACTERES Y LAS CADENAS En C++, los caracteres son los bloques de construcción fundamentales de los programas fuente. Todos los programas se componen de una secuencia de caracteres que, agrupados de manera significativa, son interpretados por la computadora como una serie de instrucciones que sirven para llevar a cabo una tarea. Un programa puede contener constantes de caracteres. Una constante de carácter es un valor entero representado corno un carácter entre apóstrofos. El valor de dicha constante es el valor entero del carácter en el conjunto de caracteres de la máquina. Por ejemplo, ‘ z’ representa el valor entero de z (122 en el conjunto de caracteres ASCII) y ‘ \n’ representa el valor entero del salto de línea (10 en el conjunto de caracteres ASCII) Una cadena es una serie de caracteres que se trata como unidad. Una cadena puede incluir letras, dígitos y diversos caracteres especiales, como +, -, *, /, $ y otros. En C++ las literales de cadena o constantes de cadena se escriben entre comillas, como sigue: “Miguel Ángel Toledo Martínez” “Ave. I.P.N. 36” “México, D. F.” “(201) 555-12121” (un nombre) (una dirección) (una ciudad y un estado) (un número telefónico) Una cadena en C++ es un arreglo de caracteres que termina con el carácter nulo (‘ \0’) Una cadena se accede por medio de un apuntador al primer carácter de dicha cadena. El valor de una cadena es la dirección (constante) de su primer carácter. Por lo tanto, en C++ es apropiado decir que una cadena es un apuntador constante; de hecho, se trata de un apuntador al primer carácter de la cadena. En este sentido, las cadenas son como los arreglos, pues un nombre de arreglo también es un apuntador (constante) a su primer elemento. En una declaración, se puede asignar una cadena a un arreglo de caracteres o a una variable de tipo char *. Las declaraciones char color[] char *colorPtr = “azul”; = “azul”; inicializan una variable con la cadena “azul”. La primera declaración crea un arreglo de 5 elementos llamado color que contiene los caracteres ‘a’, ‘z’, ‘u’, ‘l’ y ‘\’. La segunda crea una variable de apuntador colorPtr que apunta a la cadena, “azul”, que está en alguna parte de la memoria. La declaración char color [] = {“azul”}; también podría escribirse como char color[] = {‘a’, ‘z’, ‘u’, ‘l’, ‘\0' ); APUNTADORES Y CADENA – LECCIÓN 20 20-33 MIGUEL Á. TOLEDO MARTÍNEZ Al declarar un arreglo de caracteres para contener una cadena, se debe asegurar que es lo bastante grande para almacenar dicha cadena y su carácter nulo de terminación. La declaración anterior determina automáticamente el tamaño del arreglo basándose en el número de inicializadores indicados en la lista de iniciación. Es posible asignar una cadena a un arreglo por medio de extracción de flujo utilizando cin. Por ejemplo, la siguiente instrucción sirve para asignarle una cadena al arreglo de caracteres palabra[20] cin >> palabra; La cadena introducida por el usuario se almacena en palabra. La instrucción previa lee los caracteres hasta que encuentra un espacio, tabulación, salto de línea o indicador de fin de archivo. Observe que la cadena no debe ser de más de 19 caracteres, a fin de dejar espacio para el carácter nulo de terminación. El manipulador de flujo setw() que se estudió en otra lección puede servir para asegurar que la cadena introducida a palabra no exceda el tamaño del arreglo. Por ejemplo, la instrucción cin >> setw( 20 ) >> palabra; especifica que cin debe leer un máximo de 19 caracteres y ponerlos en el arreglo palabra, reservando la 20ª localidad del arreglo para el carácter nulo de terminación de la cadena. El manipulador de flujo setw() sólo se aplica al siguiente valor que se introduzca. En algunos casos es deseable introducir una línea de texto completa a un arreglo. Con este fin, C++ ofrece la función cin.getline. Ésta toma tres argumentos -un arreglo de caracteres en el que se almacenará la línea de texto, una longitud y un carácter delimitador. Por ejemplo, el segmento de programa char sentencia[80]; cin.getline( sentencia, 80, ‘\n’); declara el arreglo sentencia de 80 caracteres, luego lee del teclado una línea de texto y la carga en el arreglo. La función termina la lectura de los caracteres cuando encuentra el carácter ‘\n’, cuando se introduce un indicador de fin de archivo o cuando el número de caracteres leídos hasta el momento es de uno menos que la longitud indicada en el segundo argumento (el último carácter del arreglo se reserva para el carácter nulo de terminación) Si aparece el carácter delimitador, se lee y se descarta. El tercer argumento de cin.getline tiene ‘\n’ como valor predeterminado, por lo que la llamada de función anterior podría haberse escrito así: cin.getline( sentencia, 80 ); FUNCIONES DE MANIPULACIÓN DE CADENAS DE LA BIBLIOTECA DE MANEJO DE CADENAS La biblioteca de manejo de cadenas ofrece muchas funciones útiles para la manipulación de cadenas de datos, la comparación de cadenas, la búsqueda de caracteres y cadenas dentro de cadenas, la división de cadenas en tokens (separación de cadenas en piezas lógicas) y la determinación de la longitud de una cadena. Esta sección presenta algunas APUNTADORES Y CADENA – LECCIÓN 20 20-34 MIGUEL Á. TOLEDO MARTÍNEZ funciones comunes de manipulación de cadenas de la biblioteca de manejo de cadenas (que pertenece a la biblioteca estándar) Las funciones se resumen en la figura 20.9. Observe que varias funciones de la figura 20.9 contienen parámetros con el tipo de datos size_t. Este tipo se define en el archivo de encabezado <stddef.h> (que es un archivo de encabezado de la biblioteca estándar incluido en muchos otros archivos de encabezado de la biblioteca estándar, incluyendo <string.h>) como un tipo entero sin signo, como unsigned int y unsigned long. Prototipo de función Descripción de la función char *strcpy( char *s1, const char *s2 ) Copia la cadena s2 al arreglo de caracteres s1. Se devuelve el valor de s1. char *strncpy( char *s1, const char *s2, size_t n ) Copia cuando mucho n caracteres de la cadena s2 al arreglo de caracteres s1. Se devuelve el valor de s1. char *strcat( char *sl, const char *s2 ) Agrega la cadena s2 al final de la cadena s1. El primer carácter de s2 sobrescribe el carácter nulo de terminación de s1. Se devuelve el valor de s1. char *strncat( char *sl, const char *s2, size_t n ) Agrega cuando mucho n caracteres de la cadena s2 a la cadena s1. El primer carácter de s2 sobrescribe el carácter nulo de terminación de s1. Se devuelve el valor de s1. int strcmp( const char *sl, const char *s2 ) Compara la cadena s1 con la cadena s2. La función devuelve un valor de 0, menor que 0 o mayor que 0 si s1 es igual, menor o mayor que s2, respectivamente. Prototipo de función Descripción de la función int strncmp( const char *sl, const char *s2, size_t n ) Compara hasta n caracteres de la cadena s1 con la cadena s2. La función devuelve un valor de 0, menor que 0 o mayor que 0 si s1 es igual, menor o mayor que s2, respectivamente. char *strtok( char *sl, const char *s2 ) Una secuencia de llamadas a strtok divide la cadena s1 en "tokens" (piezas lógicas como palabras de una línea de texto) separados por los caracteres contenidos en la cadena s2. La primera llamada contiene s1 como el primer argumento y las siguientes llamadas que dividen la misma cadena en tokens contienen NULL como el primer argumento. Cada llamada devuelve un apuntador al token actual. Si no hay más tokens al llamar a la función, se devuelve NULL. size_t strlen( const char *s ) Determina la longitud de la cadena s. Devuelve el número de caracteres que precede al carácter nulo de terminación. Figura 20.9. Las funciones de manipulación de cadenas de la biblioteca manejo de cadenas. La función a strcpy() copia el segundo argumento (una cadena) al primer argumento (un arreglo de caracteres que debe ser lo bastante grande para almacenar la cadena y su carácter nulo de terminación, el cual también se copia) La función strncpy() es equivalente a strcpy(), excepto que strncpy() especifica el número de caracteres a copiar de la cadena al arreglo. Observe que la APUNTADORES Y CADENA – LECCIÓN 20 20-35 MIGUEL Á. TOLEDO MARTÍNEZ función strncpy() no necesariamente copia el carácter nulo de terminación del segundo argumento; sólo escribe un carácter nulo de terminación si el número de caracteres a copiar es, cuando menos, uno más que la longitud de la cadena. Por ejemplo, si el segundo argumento es “test”, sólo se escribe un carácter nulo de terminación si el tercer argumento de strncpy() es cuando menos 5 (los 4 caracteres de “test” más el carácter nulo determinación) Si el tercer argumento es mayor que 5, se agregan caracteres nulos al arreglo hasta que se ha escrito el número total de caracteres que indica el tercer argumento. Ejemplo 20.16 El siguiente programa: COPIAR1.CPP, utiliza strcpy() para copiar la cadena completa del arreglo x al arreglo y, y emplea strncpy() para copiar los primeros 14 caracteres del arreglo x al arreglo z. Un carácter nulo (‘\0’) se agrega al arreglo z, ya que la llamada a strncpy() no escribe un carácter nulo de terminación (el tercer argumento es menor que la longitud de la cadena del segundo argumento) /* El siguiente programa: COPIAR1.CPP, muestra el uso de strcpy() y strncpy(). */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strcpy() y strncpy() void main (void) { char x[] = "Feliz Cumpleaños a Ti"; char y[25], z[15]; cout << "La cadena de caracteres en el arreglo x es: " << x << "\nLa cadena de caracteres en el arreglo y es: " << strcpy(y, x) << '\n'; strncpy(z, x, 16); //No copia el caracter nulo z[16] = '\0'; cout << "La cadena de caracteres en el arreglo z es: " << z << endl; }//Fin de main() La función a strcat() añade el segundo argumento (una cadena) al primero (un arreglo de caracteres que contiene una cadena) El primer carácter del segundo argumento reemplaza el carácter nulo (‘\ 0’) que termina la cadena del primer argumento. El programador debe asegurarse de que el arreglo en el que se encuentra la primera cadena sea lo bastante grande para guardar la combinación de la primera y segunda cadenas, así como el carácter nulo de terminación (copiado de la segunda cadena) La función a strncat() agrega una cantidad específica de caracteres de la segunda cadena a la primera. Se agrega un carácter nulo de terminación al resultado. Ejemplo 20.17 El programa siguiente: COPIAR2.CPP, muestra las funciones strcat() y strncat() /* El siguiente programa: COPIAR2.CPP, muestra el uso de las funciones strcat() y strncat(). */ #include <iostream.h> #include <string.h> APUNTADORES Y CADENA – LECCIÓN 20 //Para cout y cin //Para strcat() y strncat() 20-36 MIGUEL Á. TOLEDO MARTÍNEZ void main(void) { char s1[20] char s2[] char s3[40] = "Feliz "; = "Año Nuevo "; = ""; cout << "s1 = " << s1 << "\ns2 = " << s2; cout << "\nstrcat(s1, s2) = " << strcat(s1, s2); cout << "\nstrncat(s3, s1, 6) = " << strncat(s3, s1, 6); cout << "\nstrcat(s3, s1) = " << strcat(s3, s1) << endl; }//Fin de main() Ejemplo 20.18 El siguiente programa, COMPARAR.CPP, compara tres cadenas por medio de strcmp() y strncmp() La función strcmp() compara el primer argumento con el segundo, carácter por carácter. La función devuelve 0 si las cadenas son iguales, un valor negativo si la primera cadena es menor que la segunda y un valor positivo si la primera cadena es mayor que la segunda. La función strncmp() es equivalente a strcmp(), excepto porque strncmp() compara hasta un número específico de caracteres. La función strncmp() no compara los caracteres que hay después de un carácter nulo. El programa imprime los valores enteros que las llamadas de la función devuelven. /* El siguiente programa: COMPARAR.CPP, muestra el uso de las funciones strcmp() y strncmp(). */ #include <iostream.h> #include <iomanip.h> #include <string.h> //Para cout y cin //Para setw() //Para strcmp() y strncmp() void main(void) { char *s1 = "Feliz Año Nuevo"; char *s2 = "Feliz Año Nuevo"; char *s3 = "Feliz Vacación"; cout << "s1 = " << s1 << "\ns2 = " << s2 << "\ns3 = " << s3 << "\n\nstrcmp(s1, s2) = " << setw(2) << strcmp(s1, s2) << "\nstrcmp(s1, s3) = " << setw(2) << strcmp(s1, s3) << "\nstrcmp(s3, s1) = " << setw(2) << strcmp(s3, s1); cout << "\n\nstrncmp(s1, s3, 6) = " << setw(2) << strncmp(s1, s3, 6) << "\nstrncmp(s1, s3, 7) = " << setw(2) << strncmp(s1, s3, 7) << "\nstrncmp(s3, s1, 7) = " << setw(2) << strncmp(s3, s1, 7) << endl; }//Fin de main() Para entender lo que significa que una cadena sea mayor que o menor que otra cadena, considere el proceso de alfabetización de una serie de apellidos. El lector, sin duda, pondría González antes que Pérez, pues en el abecedario la primera letra de González está antes que la primera letra de Pérez. Pero el abecedario es algo más que una lista de 30 letras; es una lista ordenada de caracteres. Cada letra aparece en una posición específica de la lista. La Z es algo más que una letra del abecedario; Z es, específicamente el carácter 30 del alfabeto. APUNTADORES Y CADENA – LECCIÓN 20 20-37 MIGUEL Á. TOLEDO MARTÍNEZ ¿Cómo sabe la computadora que una letra viene antes que otra? Todos los caracteres se representan dentro de la computadora como códigos numéricos; cuando la computadora compara dos cadenas, de hecho está comparando los códigos numéricos de los caracteres que hay en dichas cadenas. Con el fin de estandarizar las representaciones de los caracteres, la mayoría de los fabricantes de computadoras han diseñado sus máquinas para que operen con alguno de dos esquemas de codificación: el ASCII (Código Estándar Americano para el Intercambio de Información) o el EBCDIC (Código de Intercambio Decimal Codificado en Binario Extendido) Hay otros esquemas de codificación, pero estos dos son los más difundidos. ASCII y EBCDIC se llaman códigos de caracteres o conjuntos de caracteres. Las manipulaciones de cadenas y caracteres de hecho comprenden la manipulación de los códigos numéricos correspondientes, y no de los caracteres mismos. Esto explica por qué los caracteres y los enteros pequeños son intercambiables en C++. Debido a que tiene sentido decir que un código numérico es mayor que, menor que o igual a otro código numérico, es posible relacionar varios caracteres o cadenas entre ellos haciendo referencia a los códigos de caracteres. La función strtok() sirve para dividir una cadena en una serie de tokens. Un token es una secuencia de caracteres separada por caracteres delimitadores (por lo general espacios o marcas de puntuación) Por ejemplo, en una línea de texto, cada palabra puede considerarse como un token y los espacios que las separan pueden verse como delimitadores. Se necesitan varias llamadas a strtok() para dividir en tokens una cadena (suponiendo que la cadena contenga más de un token) La primera llamada a strtok() contiene dos argumentos: una cadena a dividir en tokens y una cadena que contiene los caracteres que separan los tokens (es decir, delimitadores) En el siguiente programa, TOKENS.CPP, la instrucción tokenPtr = strtok( cadena, “ “l); asigna tokenPtr como apuntador al primer token de cadena. El segundo argumento de strtok(), “ ”, indica que los tokens en cadena están separados por espacios. La función strtok() busca el primer carácter de cadena que no sea un carácter delimitador (espacio) Éste es el inicio del primer token. Luego encuentra el siguiente carácter delimitador de la cadena y lo reemplaza con un carácter nulo ( ‘\ 0’ ) Esto termina el token actual. La función a strtok() guarda un apuntador al carácter que sigue al token y devuelve un apuntador al token actual. Las siguientes llamadas a strtok() que continúan dividiendo en tokens a cadena contienen NULL como primer argumento. El argumento NULL indica que la llamada a strtok() deberá continuar dividiendo en tokens a partir de la localidad de cadena guardada por la última llamada a strtok() Si no quedan tokens cuando se llame a strtok(), éste devuelve NULL. Ejemplo 20.19 El programa TOKENS.CPP, utiliza strtok() para dividir en tokens la cadena “Esta es una oración con 7 tokens”. Cada token se imprime por separado. Observe que strtok() modifica la cadena de entrada, por lo tanto, debe hacer una copia de la cadena si desea volver a utilizarla en el programa tras las llamadas a strtok() /* El siguiente programa: TOKENS.CPP, muestra el uso de la función strtok(). */ #include <iostream.h> #include <string.h> APUNTADORES Y CADENA – LECCIÓN 20 //Para cout y cin //Para strtok() 20-38 MIGUEL Á. TOLEDO MARTÍNEZ void main(void) { char cadena[] = "Esta es una oración con 7 tokens"; char *tokenPtr; cout << "La cadena a dividirse en tokens es:\n" << cadena << "\n\nLos tokens son:\n"; tokenPtr = strtok(cadena, " "); while(tokenPtr != NULL) { cout << tokenPtr << '\n'; tokenPtr = strtok(NULL, " "); }//Fin de while }//Fin de main() La función strlen() toma como argumento una cadena y devuelve el número de caracteres que ésta contiene –en la longitud no se incluye el carácter nulo de terminación. Ejemplo 20.20 El programa siguiente, LONGITUD.CPP, muestra el uso de la función strlen() /* El siguiente programa: LONGITUD.CPP, muestra el empleo de la función strlen(). */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strlen() void main(void) { char *cadena1 = "abcdefghijklmnopqrstuvwxyz"; char *cadena2 = "cinco"; char *cadena3 = "México"; cout << "La longitud de \"" << cadena1 << "\" es " << strlen(cadena1) << "\nLa longitud de \"" << cadena2 << "\" es " << strlen(cadena2) << "\nLa longitud de \"" << cadena3 << "\" es " << strlen(cadena3) << endl; }//Fin de main() A continuación se resuelven una serie de ejercicios con la intención de que el lector estudie por si mismo lo que los programas hacen y como lo hacen. Independiente de lo anterior en el mismo programa se da una breve definición del problema. APUNTADORES Y CADENA – LECCIÓN 20 20-39 MIGUEL Á. TOLEDO MARTÍNEZ EJERCICIOS RESUELTOS /* El siguiente programa: ALFABETO.CPP, define una cadena de 256 caracteres y luego se le agrega las letras mayúsculas del alfabeto. */ #include <iostream.h> //Para cout y cin void main (void) { char cadena[256]; int i; for (i = 0; i < 26; i++) cadena[i] = 'A' + i; cadena[i] = NULL; cout << "El contenido de la cadena es: " << cadena; }//Fin de main() /* El siguiente programa: ALFABETO2.CPP, asigna las letras mayúsculas de la A a la Z a una cadena de caracteres. Posteriormente se agrega el caracter NULL al elemento cadena[10]. */ #include <iostream.h> //Para cout y cin void main (void) { char cadena[256]; int i; for (i = 0; i < 26; i++) cadena[i] = 'A' + i; cadena[10] = NULL; cout << "La cadena contiene: " << cadena; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-40 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: COMILLAS.CPP, usa la secuencia de escape \" para colocar comillas dentro de la declaración de una constante. */ #include <iostream.h> //Para cout y cin void main(void) { char cadena[] = "\"!Alto!\", dijo él."; cout << cadena; }//Fin de main() /* El siguiente programa: GETLINE.CPP, utiliza el objeto getline() para leer una serie de caracteres desde el teclado. Posteriormente visualiza esta cadena de caracteres, caracter por caracter. Finalmente muestra la longitud de la cadena. */ #include <iostream.h> //Para cout y cin void main (void) { char cadena[256]; int i; // Cadena introducida por el usuario // Índice a utilizar en la cadena cout << "Digite una cadena de caracteres y oprima enter: "; cin.getline(cadena, sizeof(cadena), '\n'); // Visualizar cada elemento de la cadena hasta que encuentre el caracter NULL for (i = 0; cadena[i] != NULL; i++) cout << cadena[i]; cout << endl << "El número de caracteres de la cadena es de: " << i; }//Fin de main() /* El siguiente programa: LONGITUD2.CPP, ilustra el uso de la función strlen(). Esta función regresa la cantidad de caracteres que hay en el parámetro cadena. El tipo resultante size_t tiene un typedef de tipo unsigned int. */ #include <iostream.h> #include <string.h> APUNTADORES Y CADENA – LECCIÓN 20 //Para cout y cin //Para strlen() 20-41 MIGUEL Á. TOLEDO MARTÍNEZ void main(void) { char tituloLibro[] = "\"El Coronel no tiene quien le escriba\""; cout << tituloLibro << " contiene " << strlen(tituloLibro) << " caracteres"; }//Fin de main() /* ************************************************************************ Una posible implementación de la función strlen(), es la siguiente: size_t strlen(const char *cadena) { int i = 0; while (cadena[i]) i++; return (i) }//Fin de strlen() **************************************************************************** */ /* El siguiente programa: COPIAR3.CPP, ilustra el uso de la función strcpy(). El prototipo para la función strcpy es: char *strcpy(char *destino, const char *fuente) La función copia los caracteres de la cadena fuente a la cadena destino. La función supone que la cadena de destino tiene suficiente espacio para contener la cadena de origen. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strcpy() void main(void) { char titulo[] = "\"El Coronel no tiene quien le escriba\""; char libro[128]; strcpy(libro, titulo); cout << "El nombre del libro es: " << libro << endl; }//Fin de main() /* *********************************************************************** Una implementación de la función strcpy() podría ser la siguiente: char *strcpy(char *destino, const char *fuente) { while (*destino++ = *fuente++) ; return (destino - 1); }//Fin de *strcpy() **************************************************************************/ /* El siguiente programa: ANEXAR.CPP, ilustra el uso de la función strcat(). La función añade el contenido de la cadena fuente a la cadena de destino y regresa el apuntador a la cadena de destino. La función asume que la cadena de destino puede acomodar los caracteres de la cadena de origen. */ APUNTADORES Y CADENA – LECCIÓN 20 20-42 MIGUEL Á. TOLEDO MARTÍNEZ #include <iostream.h> #include <string.h> //Para cout y cin //Para strcat() void main(void) { char nombre[64] = "Miguel Angel es tan"; strcat(nombre, " feliz"); cout << nombre; }//Fin de main() /* *********************************************************************** Una posible implementación de la función strcat es la siguiente: char *strcat(char *destino, const char *fuente) { char *original = destino; while (*destino) destino**; // Busca el final de la cadena while (*destino++ = *fuente++) ; return (original) }//Fin de *strcat() ************************************************************************** */ /* El siguiente programa: ANEXAR2.CPP, ilustra el uso de la función strncat(). La función añade num caracteres de la cadena origen a la cadena de destino y regresa un apuntador a la cadena de destino. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strncat() void main(void) { char nombre[64] = "Miguel Angel"; strncat(nombre, " Martínez", 4); cout << "¿Votaste por?: " << nombre << endl; }//Fin de main() /* *********************************************************************** Una posible implementación de la función strncat() es la siguiente: char *strncat(char *destino, const char *fuente, int n) { char *original = destino; int i = 0; while (*destino) destino++; while ((i++ < n) && (*destino++ = *fuente++)) ; APUNTADORES Y CADENA – LECCIÓN 20 20-43 MIGUEL Á. TOLEDO MARTÍNEZ if (i > n) *destino = NULL; return (original); }//Fin de strncat() ************************************************************************** */ /* El siguiente programa: STRXFRM.CPP, ilustra el uso de la función strxfrm() */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strxfrm() void main(void) { char fuente[64] = "\El Coronel no tiene quien le escriba\""; char destino[64]; int longitud; longitud = strxfrm(destino, fuente, sizeof(fuente)); cout << "Longitud de la cadena: " << longitud << endl; cout << "Contenido del destino: " << destino << endl; cout << "Contenido de la fuente: " << fuente << endl; }//Fin de main() /* El siguiente programa: STREQL.CPP, ilustra el uso de la función streql() */ #include <iostream.h> //Para cout y cin int streql(char *cadena1, char *cadena2) { while ((*cadena1 == *cadena2) && (*cadena1)) { cadena1++; cadena2++; }//Fin de while return((*cadena1 == NULL) && (*cadena2 == NULL)); }//Fin de streql() void main(void) { cout << "Comparando Abc y Abc : " << streql("Abc", "Abc") << endl; cout << "Comparando abc y Abc : " << streql("abc", "Abc") << endl; cout << "Comparando abcd y abc: " << streql("abcd", "abc") << endl; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-44 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: STRIEQL.CPP, ilustra el uso de la función strieql() */ #include <iostream.h> #include <ctype.h> //Para cout y cin //Para toupper() int strieql(char *cadena1, char *cadena2) { while ((toupper(*cadena1) == toupper(*cadena2)) && (*cadena1)) { cadena1++; cadena2++; }//Fin de strieql() return((*cadena1 == NULL) && (*cadena2 == NULL)); }//Fin de strieql() void main(void) { cout << "Comparando Abc y Abc : " << strieql("Abc", "Abc") << endl; cout << "Comparando abc y Abc : " << strieql("abc", "Abc") << endl; cout << "Comparando abcd y abc: " << strieql("abcd", "abc") << endl; }//Fin de main() /* El siguiente programa: STRLWR.CPP, ilustra el uso de la funciones strlwr() y strupr(). */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strlwr() y strupr() void main(void) { cout << strlwr("\"El Coronel no tiene quien le escriba\"") << endl; cout << strupr("\"El Coronel no tiene quien le escriba\"") << endl; }//Fin de main() /* ********************************************************************* Una posible implementación de la función strlwr() es la siguiente: #include <ctype.h> char *strlwr(char *cadena) { char *original = cadena; while (*cadena) { *cadena = tolower(*cadena); cadena++; }//Fin de while return(original) }//Fin de *strlwr() ************************************************************************** */ APUNTADORES Y CADENA – LECCIÓN 20 20-45 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: STRCHR.CPP, ilustra el uso de la función strchr */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strchar() void main(void) { char titulo[64] = "\"El Coronel no tiene quien le escriba\""; char *ptr; if (ptr = strchr(titulo, 'C')) cout << "La primera ocurrencia de C es en el desplazamiento: " << (ptr - titulo) << endl; else cout << "¡NO SE ENCONTRÓ EL CARÁCTER BUSCADO!" << endl; }//Fin de main() /* *********************************************************************** Una posible implementación de la función strchr() es la siguiente: char *strchr(const char *cadena, int letra) { while ((*cadena != letra) && (*cadena) cadena++; return(cadena); }//Fin de *strchr() ************************************************************************** */ /* El siguiente programa: STRRCHR.CPP, ilustra el uso de la función strrchr */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strrchr() char *strrchr(char *cadena, char letra) { char *ptr = NULL; while (*cadena) { if (*cadena == letra) ptr = cadena; cadena++; }//Fin de while return(ptr); }//Fin de *strrchr() void main(void) { char titulo[64] = "\"El Coronel no tiene quien le escriba\""; char *ptr; if (ptr = strrchr(titulo, 'n')) cout << "La última vez en que se encontró la letra n a la derecha: " << (ptr - titulo) << endl; else cout << "!NO SE ENCONTRÓ EL CARÁCTER BUSCADO!" << endl; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-46 MIGUEL Á. TOLEDO MARTÍNEZ /* *********************************************************************** Una posible implementación de la función strrchr() es la siguiente: char *strrchr(const char *cadena, int letra) { char *ptr = NULL; while (*cadena) { if (*cadena == letra) ptr = cadena; cadena++; }//Fin de while return(ptr); }//Fin de *strrch() ************************************************************************** */ FUNCIONES DE CADENA QUE UTILIZAN CADENAS UBICADAS FUERA DE LOS 64KB (far string) Algunos compiladores poseen funciones que aceptan apuntadores ubicados fuera de los 64 kbyes. Por ejemplo, para determinar la longitud de una cadena referenciada por un apuntador far, puede utilizar la función _fstrlen(), como se muestra a continuación: #include <string.h> size_t _fstrlen(const char *string) En el caso de que no exista en el compilador y usted tenga una función que maneje apuntadores ubicados en los 64 kbytes, puede modificar los mismos como se muestra en el siguiente ejemplo, para la función fstreql(): int fstreql (char far *str1, char far *str2) { while ((*str1 == *str2) && (*str1)) { str1++; str2++; }//Fin de while return ((*str1 == NULL) && (*str2 ==NULL)); }//Fin de fstreql() NUMERO DE OCURRENCIAS DE UN CARÁCTER DENTRO DE UNA CADENA. A continuación escribiremos la función chrcnt() que resuelve el problema planteado: int chrcnt (const char *cadena, int letra) { int contador = 0; while (*cadena) if (*cadena == letra) contador++; return (contador); APUNTADORES Y CADENA – LECCIÓN 20 }//Fin de chrcnt() 20-47 MIGUEL Á. TOLEDO MARTÍNEZ A continuación escribiremos la función strrev() que invierta una cadena. char *strrev (char *cadena) { char *original = cadena; char *adelante = cadena; char temp; while (*cadena) cadena++; while (adelante < cadena) { temp = *(--cadena); *cadena = *adelante; *adelante++ = temp; }//Fin de while() return (original); }//Fin de *strrev() A continuación escribiremos la función strset() sustituye todos los caracteres de una cadena por un carácter especificado. char * strset (char *cadena, int letra) { char *original = cadena; while (*cadena) *cadena++ = letra; return (original); }//Fin de *strset() /* El siguiente programa: STRCMP.CPP, ilustra el uso de la función strcmp(), la cual compara dos cadenas por menor, mayor o igual. Devolviendo 0 si ambas son iguales, si la primera es mayor que la segunda devuelve un número negativo, finalmente si la segunda es mayor que la primera devuelve un número positivo. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strcmp() void main(void) { cout << "Comparando Abc y Abc : " << strcmp("Abc", "Abc") << endl; cout << "Comparando abc y Abc : " << strcmp("abc", "Abc") << endl; cout << "Comparando abcd y abc: " << strcmp("abcd", "abc") << endl; cout << "Comparando Abc y Abcd: " << strcmp("Abc", "Abcd") << endl; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-48 MIGUEL Á. TOLEDO MARTÍNEZ /* *********************************************************************** Una posible implementación de la función strcmp() es la siguiente: int strcmp(const char *s1, const char *s2) { while ((*s1 == *s2) && (*s1)) { s1++; s2++; }//Fin de while if ((*s1 == *s2) && (! *s1) // Las cadenas son iguales return(o); else if ((*s1) && (! *s2)) // cadena s1 > cadena s2 return(-1); else if ((*s2) && (! *s1)) // cadena s2 > cadena s1 return(1); else return ((*s1 > *s2) ? -1; 1); }//Fin de strcmp() ************************************************************************** */ /* El siguiente programa: STRNCMP.CPP, muestra la función strncpm(), la cual compara cierto numero de caracteres de una cadena con otra. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strncmp() void main(void) { cout << "Comparando tres letras de Abc con Abc: " << strncmp("Abc", "Abc", 3) << endl; cout << "Comparando tres letras de abc con Abc: " << strncmp("abc", "Abc", 3) << endl; cout << "Comparando tres letras de abcd con abc: " << strncmp("abcd", "abc", 3) << endl; cout << "Comparando cinco letras de Abc con Abcd: " << strncmp("Abc", "Abcd", 5) << endl; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-49 MIGUEL Á. TOLEDO MARTÍNEZ /* *********************************************************************** Una posible implementación de la función strncmp() es la siguiente: int strncmp(const char *s1, const char *s2, int n) { int i = o; while ((*s1 == *s2) && (*s1) && i < n) { s1++; s2++; i++ }//Fin de while if (i == n) // Las cadenas son iguales return (0); else if ((*s1 == *s2) && (! *s1) // Las cadenas son iguales return(o); else if ((*s1) && (! *s2)) // cadena s1 > cadena s2 return(-1); else if ((*s2) && (! *s1)) // cadena s2 > cadena s1 return(1); else return ((*s1 > *s2) ? -1; 1); }//Fin de strncmp() ************************************************************************** */ /* El siguiente programa: CMPCASE.CPP, ilustra el uso de las funciones stricmp() y strncmpi(), las cuales hacen comparaciones sin diferenciar mayúsculas de minúsculas. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para stricmp() y strncmpi() void main(void) { cout cout cout cout << "Comparando Abc con Abc: " << stricmp("Abc", "Abc") << endl; << "Comparando abc con Abc: " << stricmp("abc", "Abc") << endl; << "Comparando tres letras de abcd con ABC: " << strncmpi("abcd", "ABC", 3) << endl; << "Comparando 5 letras de abc con Abcd: " << strncmpi("abc", "Abcd", 5); }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-50 MIGUEL Á. TOLEDO MARTÍNEZ /* La mayoría de los compiladores ofrecen un conjunto de funciones para convertir caracteres ASCII a números, véase la tabla siguiente: función atof() atoi() atol() strtod() strtol() propósito Convierte de ascii a punto flotante. Convierte de ascii a entero. Convierte de ascii a entero largo. Convierte de ascii a doble precisión. Convierte de ascii a entero largo. El siguiente programa: ASCIINUM.CPP, ilustra el uso de algunas de las funciones anteriores. */ #include <iostream.h> #include <stdlib.h> #include <iomanip.h> //Para cout y cin //Para las funciones anteriores //Para setprecision() void main(void) { int intResultado; float floatResultado; long longResultado; intResultado = atoi("1234"); floatResultado = atof("12345.678"); longResultado = atol("1234567L"); cout << intResultado << ' ' << setprecision(8) << floatResultado << ' ' << longResultado; }//Fin de main() /* El siguiente programa: STRDUP.CPP, ilustra el uso de la función strdup() para copiar una cadena de caracteres a un área dinámica de la memoria. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strdup() void main(void) { char *titulo; if ((titulo = strdup("\"El Coronel no tiene quien le escriba\""))) cout << "Título: " << titulo << endl; else cout << "Error al duplicar la cadena" << endl; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-51 MIGUEL Á. TOLEDO MARTÍNEZ /* *********************************************************************** Una posible implementación de la función strdup() es la siguiente: #include <string.h> #include <malloc.h> //Para cout y cin //Para malloc() char *strdup(const char *s1) { char *ptr; if ((ptr = malloc(strlen(s1)))) strcpi(ptr,s1); // Localiza area dinámica de memoria return(ptr); }//Fin de *strdup() ************************************************************************** */ /* El siguiente programa: STRSPN.CPP, ilustra el uso de la función strspn(). Esta función busca en una cadena por la primera ocurrencia de cualquiera de los caracteres de una subcadena. La función devuelve el desplazamiento dentro de la cadena del primer caracter que no esta contenido en la segunda cadena especificada. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strspn() void main(void) { cout << "Buscando la subcadena Abc en la cadena AbcDef: " << strspn("AbcDef", "Abc") << endl; cout << "Buscando la subcadena cbA en la cadena AbcDef: " << strspn("AbcDef", "cbA") << endl; cout << "Buscando la subcadena Def en AbcAbc: " << strspn("AbcAbc", "Def"); }//Fin de main() /* ********************************************************************** Una posible implementación de la función strspn es la siguiente: size_t strspn(const char *s1, const char *s2) { int i, j; for (i = 0; *s1; i++, s1++) { for (j = 0; s2[j]; j++) if (*s1 == s2[j]) break; if (s2[j] == NULL); break; }//Fin del for return (i); }//Fin de sstrspn() ************************************************************************** */ APUNTADORES Y CADENA – LECCIÓN 20 20-52 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: STRSTR.CPP, ilustra el uso de la función strstr(), la cual busca una subcadena dentro de una cadena. Si la encuentra devuelve un apuntador a la primera ocurrencia de la subcadena. Si no la encuentra devuelve NULL. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strstr() void main(void) { cout << "Buscando la subcadena Abc en la cadena AbcDef: " << (strstr("AbcDef", "Abc") ? "Encontrado" : "No encontrado") << endl; cout << "Buscando la subcadena Abc en la cadena abcDef: " << (strstr("abcDef", "Abc") ? "Encontrado" : "No encontrado") << endl; cout << "Buscando la subcadena Abc en la cadena AbcAbc: " << (strstr("AbcAbc", "Abc") ? "Encontrado" : "No encontrado"); }//Fin de main() /* *********************************************************************** Una posible implementación de la función strstr() es la siguiente: char *strstr(const char *s1, const char *s2) { int i, j, k; for (i = 0; s1[i]; i++) for (j = i, k = 0; s1[j] == s2[k]; j++, k++) if (! s2[k + 1]) return (s1 + i); return (NULL); }//Fin de *strstr() ************************************************************************** */ CONTAR EL NÚMERO DE OCURRENCIA DE UNA SUBCADENA DENTRO DE UNA CADENA La siguiente función: strstr_cnt() cuenta el número de veces en el que una subcadena se encuentra dentro de una cadena. int strstr_cnt (const char *cadena, const char *subcadena) { int i, j, k, contador = 0; for (i = 0; cadena[i]; i++) for (j = i, k = 0; cadena[j] == subcadena[k]; j++, k++) if (! subcadena[k + 1]) contador++; return (contador); }//Fin de strstr_cnt() APUNTADORES Y CADENA – LECCIÓN 20 20-53 MIGUEL Á. TOLEDO MARTÍNEZ OBTENER UN ÍNDICE A UNA SUBCADENA La siguiente función strstr_index(), devuelve el índice donde se encuentra la subcadena dentro de la cadena: char *strstr_index(const char *s1, const char *s2) { int i, j, k; for (i = 0; s1[i]; i++) for (j = i, k = 0; s1[j] == s2[k]; j++, k++) if (! s2[k + 1]) return (i); return (i); }//Fin de *strstr_index() OBTENER LA OCURRENCIA MAS A LA DERECHA DE UNA SUBCADENA La función r_strstr() devuelve un apuntador a la ocurrencia de más a la derecha de una subcadena dentro de una cadena: char *r_strstr(const char *s1, const char *s2) { int i, j, k , izquierda = 0; for (i = 0; s1[i]; i++) for (j = i, k = 0; s1[j] == s2[k]; j++, k++) if (! s2[k + 1]) izquierda = i; return ((izquierda) ? s1 + izquierda: NULL); }//Fin de *r_strstr() REMOVER UNA SUBCADENA CONTENIDA DENTRO DE UNA CADENA La siguiente función strstr_rem() remueve la primera ocurrencia de una subcadena contenida en una cadena. char *strstr_rem (char *cadena, char *subCadena) { int i, j, k, loc = -1; for (i = 0; cadena[i] && (loc == -1); i++) for (j =i, k = 0; str[j] == subCadena[k]; j++, k++) if (! subCadena[k + 1] ) loc = i; if (loc != -1) { // La cadena fue encontrada for (k = 0; subCadena[k]; k++) for (j = loc, i = loc + k; cadena[i]; j++, i++) cadena[j] = cadena[i]; APUNTADORES Y CADENA – LECCIÓN 20 20-54 MIGUEL Á. TOLEDO MARTÍNEZ cadena [i] == NULL; }//Fin de if return (cadena); }//Fin de *strstr_rem REEMPLAZO DE UNA SUBCADENA POR OTRA La siguiente función: strstr_rep(), reemplaza la primera ocurrencia de una subcadena por otra subcadena. #include <string.h> char *strsstr_rep (char *fuente, char *viejo, char *nuevo) { char *original = fuente; char temp[256]; int longitudVieja = strlen(viejo); int i, j, k, localizacion = -1; for (i = 0; fuente[i] && (localizacion == -1); ++i) for (j = i, k = 0; fuente[j] == viejo[k]; j++, k++) if (! viejo[k + 1]) localizacion = i; if (localizacion != -1) { for (j = 0; j < localizacion; j++) temp[j] = fuente[j]; for (i = 0; nuevo[i]; i++, j++) temp[j] = nuevo[i]; for (k = localizacion + longitudVieja; fuente[k]; k++, j++) temp[j] = fuente[k]; temp[j] = NULL; for (i = 0; fuente[i] = temp[i]; i++)//Ciclo nulo ; }//Fin de if return (original); } //Fin de *strstr_rep() APUNTADORES Y CADENA – LECCIÓN 20 20-55 MIGUEL Á. TOLEDO MARTÍNEZ DETERMINAR SI UN CARÁCTER ES ALFANUMÉRICO Un carácter es alfanumérico si es letra o dígito. El archivo de cabecera ctype.h contiene una macro llamada isalnum que determina si un carácter es alfanumérico: if (isalnum(letra)) Considere la siguiente implementación de dicha macro: #define isalnum(c) ((toupper( (c) ) >= ‘A’ ) && (toupper ((c)) <= ‘Z’) || ((c) >= ‘0’ ) && ((c) <= ‘9’)) DETERMINAR SI UN CARÁCTER ES UNA LETRA DEL ALFABETO El archivo de cabecera ctype.h proporciona la macro isalpha para determinar si un carácter es una letra: if (isalpha(carácter)) Considere la siguiente implementación de dicha macro: #define isalpha(c) (toupper((c)) >= ‘A’ && (toupper((c)) <= ‘Z’) DETERMINAR SI UN CARÁCTER CONTIENE UN VALOR ASCII Un valor es ASCII si se encuentra en el rango comprendido entre 0 y 127. El archivo cabecera ctype.h contiene la macro isascii que le ayuda a determinar si un carácter contiene un valor ASCII: if (isascii(carácter)) Considere la siguiente implementación de dicha macro: #define isascii(ltr) ((unsigned) (ltr) < 128) DETERMINAR SI UN CARÁCTER ES UN CARÁCTER DE CONTROL Un carácter de control es un valor comprendido entre ^A y ^Z o ^a y ^z. El archivo cabecera ctype.h contiene la macro iscntrl que le ayuda a determinar si un carácter es de control: if (iscntrl(carácter)) DETERMINAR SI UN CARÁCTER ES UN DIGITO Un dígito es un valor ASCII comprendido entre 0 y 9. El archivo de cabecera ctype.h contiene la macro isdigit que le ayuda a determinar si un carácter es o no dígito: if (isdigit(carácter)) Considere la siguiente implementación de dicha macro: #define isdigit(c) ((c) >= ‘0’ && (c) <= ‘9’) APUNTADORES Y CADENA – LECCIÓN 20 20-56 MIGUEL Á. TOLEDO MARTÍNEZ DETERMINAR SI UN CARÁCTER ES UN CARÁCTER GRAFICO Un carácter es gráfico si es un carácter imprimible (ver isprint), excluyendo el carácter espacio (ASCII 32) El archivo de cabecera ctype.h contiene la macro isgraph que le ayuda a determinar si un carácter es o no gráfico: if (isgraph(carácter)) Considere la siguiente implementación de dicha macro: #define isgraph(ltr) ((ltr) >=33) && ((ltr) <= 127) DETERMINAR SI UN CARÁCTER ES MAYÚSCULA O MINÚSCULA El archivo de cabecera ctype.h contiene las macros islower y isupper que le ayudan a determinar si un carácter es minúscula o mayúscula respectivamente: if (islower(carácter)) if(isupper(carácter)) Considere las siguiente implementaciones de dichas macros: #define islower(c) ((c) >= ‘a’ && (c) <= ‘z’) #define isupper(c) ((c) >= ‘A’ && (c) <= ‘Z’) DETERMINAR SI UN CARÁCTER ES IMPRIMIBLE Un carácter es imprimible si se encuentra en el rango comprendido entre 32 y 127. El archivo de cabecera ctype.h contiene la macro isprint que le ayuda a determinar si un carácter es o no imprimible: if (isprint(carácter)) Considere la siguiente implementación de dicha macro: #define isprint(ltr) ((ltr) >=32) && ((ltr) <= 127) DETERMINAR SI UN CARÁCTER ES UN SÍMBOLO DE PUNTUACIÓN Desde el punto de vista gramatical los símbolos de puntuación incluyen a la coma, punto y coma, punto, interrogación y así sucesivamente. Desde punto de vista del lenguaje C un símbolo es de puntuación si es un carácter gráfico que no sea alfanumérico. El archivo de cabecera ctype.h contiene la macro ispunct que le ayuda a determinar si un carácter es o no símbolo de puntuación: if (ispunct(carácter)) Considere la siguiente implementación de dicha macro: #define ispunct(c) (isgraph(c)) && ! isalphanum((c))) APUNTADORES Y CADENA – LECCIÓN 20 20-57 MIGUEL Á. TOLEDO MARTÍNEZ DETERMINAR SI UN CARÁCTER ES EL CARÁCTER ESPACIO El carácter espacio incluye el espacio, tabulador, retorno de carro, nueva línea, tabulador vertical y alimentación de hoja. El archivo de cabecera ctype.h contiene la macro isspace que le ayuda a determinar si un carácter es o no carácter espacio: if (isspace(carácter)) Considere la siguiente implementación de dicha macro: #define isspace(c) (((c) == 32) || ((c) == 9) || ((c) == 13)) DETERMINAR SI UN CARÁCTER ES UN VALOR HEXADECIMAL Un carácter es hexadecimal si es un dígito entre 0 y 9 o una letra entre A y F. El archivo de cabecera ctype.h contiene la macro isxdigit que le ayuda a determinar si un carácter es o no un carácter hexadecimal: if (isxdigit(carácter)) Considere la siguiente implementación de dicha macro: #define isxdigit(c) (isnum((c)) || (toupper((c)) >= ‘A’ && (toupper((c)) <= ‘F’)) /* El siguiente programa: TOUPPER.CPP, ilustra el uso de la macro _toupper y la función toupper. Se observa el error que puede ocurrir cuando se utilizan letras mayúsculas con la macro _toupper. */ #include <stdio.h> #include <ctype.h> //Para putchar //Para _toupper() y toupper() void main (void) { char cadena[] = "\"El Coronel No Tiene Quien Le Escriba\""; int i; for (i = 0; cadena[i]; i++) putchar(toupper(cadena[i])); putchar('\n'); for (i = 0; cadena[i]; i++) putchar(_toupper(cadena[i])); putchar('\n'); }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-58 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: TOLOWER.CPP, ilustra el uso de la macro _tolower y la función tolower. Se observa el error que puede ocurrir cuando se utilizan letras minúscula con la macro _tolower. */ #include <stdio.h> #include <ctype.h> //Para putchar() //Para tolower() y _tolower() void main (void) { char cadena[] = "\"El Coronel No Tiene Quien Le Escriba\""; int i; for (i = 0; cadena[i]; i++) putchar(tolower(cadena[i])); putchar('\n'); for (i = 0; cadena[i]; i++) putchar(_tolower(cadena[i])); putchar('\n'); }//Fin de main() CARÁCTER ASCII VÁLIDO Para asegurar que un carácter es un carácter ASCII válido, su valor se encuentra entre 0 y 127 utilice la macro toascii contenida en archivo de cabecera ctype.h: Una implementación de dicha macro es la siguiente: #define toascii(character) ((character) & 0x7F) Lo que se logra con el valor hexadecimal 0x7F es hacer que el valor ASCII sea siempre positivo (0 a 127) /* La función printf, le permite escribir salida formateada a la pantalla. Dependiendo de los requerimientos de su programa, existen ocasiones en que necesita trabajar con una cadena de caracteres que contiene salida formatea da. Por ejemplo, suponga que sus empleados tienen 5 dígitos como número de empleado y tres caracteres que identifican la región (tal como OAX para Oaxaca). Suponga que almacena información acerca de cada empleado en un archivo cuyo nombre es una combinación de estos dos valores (tal como OAX12345). La función sprintf le permite escribir salida formateada en una cadena de caracteres. El siguiente programa: SPRINTF.CPP, utiliza la función sprintf para crear un nombre de archivo de 8 caracteres. */ #include <stdio.h> //Para la función sprintf() y printf() APUNTADORES Y CADENA – LECCIÓN 20 20-59 MIGUEL Á. TOLEDO MARTÍNEZ void main(void) { int numeroEmpleado = 12345; char region[] = "OAX"; char nombreArchivo[64]; sprintf(nombreArchivo, "%s%d", region, numeroEmpleado); printf("Nombre del empleado: %s\n", nombreArchivo); }//Fin de main() /* La función scanf() le permite leer entrada formateada desde stdin. Dependiendo de su programa, existen ocasiones en las cuales una cadena de caracteres contiene campos que desea asignar a variables específicas. La función sscanf() le permite leer valores de una cadena, asignando los valores a variables específicas. El siguiente programa: SSCANF.CPP, ilustra el uso de la función sscanf(). */ #include <stdio.h> //Para sscanf() y printf() void main(void) { int edad; float salario; char cadena[] = "33 25000.00"; sscanf(cadena, "%d %f\n", &edad, &salario); printf("Edad: %d salario %f\n", edad, salario); }//Fin de main() /* El siguiente programa: CADENA1.CPP, utiliza las funciones strlen(), strcat() busca un caracter en una cadena y lo remplaza por otro caracter. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strlen(), strcat() const unsigned MAX1 = 40; const unsigned MAX2 = 80; void main(void) { char cadenaPequena[MAX1 + 1]; char cadenaGrande[MAX2 + 1]; char caracterBuscar, caracterRemplazar; cout << "Introduzca la cadena pequeña: "; cin.getline(cadenaPequena, MAX1); cout << "Introduzca la cadena grande : "; cin.getline(cadenaGrande, MAX2); cout << endl << endl; APUNTADORES Y CADENA – LECCIÓN 20 20-60 MIGUEL Á. TOLEDO MARTÍNEZ cout cout << "La cadena pequena tiene " << strlen(cadenaPequena) << " caracteres" << endl; << "La cadena grande tiene " << strlen(cadenaGrande) << " caracteres" << endl << endl; // Encadena cadenaPequena a cadenaGrande strcat(cadenaGrande, cadenaPequena); cout cout << "Las cadenas encadenadas son: " << cadenaGrande << endl; << "La nueva cadena tiene " << strlen(cadenaGrande) << " caracteres" << endl << endl; // Obtiene los caracteres de búsqueda y reemplazo cout << "Introduzca el caracter a buscar: "; cin >> caracterBuscar; cout << "Introduzca el caracter que reemplaza: "; cin >> caracterRemplazar; // Reemplaza caracter en la cadena cadenaGrande for (int i = 0; i < strlen(cadenaGrande); i++) if (cadenaGrande[i] == caracterBuscar) cadenaGrande[i] = caracterRemplazar; // Despliega la cadena cadenaGrande actualizada cout << "\nLa nueva cadena es: " << cadenaGrande; }//Fin de main() /* El siguiente programa: CADENA2.CPP, utiliza el método de ordenamiento de la burbuja para ordenar un arreglo de cadenas de caracteres. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strcmp(), strcpy() const unsigned TAMANO_CADENA = 40; const unsigned TAMANO_ARREGLO = 11; void main(void) { char cadena[TAMANO_ARREGLO][TAMANO_CADENA] = { "California", "Virginia", "Alaska", "New York", "Michigan", "Nevada", “Ohio", "Florida", "Washington", "Oregon", "Arizona" }; cout << "El arreglo desordenado es:" << endl; for (int i = 0; i < TAMANO_ARREGLO; i++) cout << cadena[i] << endl; cout << endl; // Usa el método de ordenamiento de la burbuja para ordenar el arreglo for (int i = 0; i < TAMANO_ARREGLO; ++i) for (int j = 0; j < i; ++j) if (strcmp(cadena[i], cadena[j]) < 0) APUNTADORES Y CADENA – LECCIÓN 20 20-61 MIGUEL Á. TOLEDO MARTÍNEZ { char temp[TAMANO_CADENA]; strcpy(temp, cadena[i]); strcpy(cadena[i], cadena[j]); strcpy(cadena[j], temp); }//Fin de if cout << "El arreglo ordenado es:" << endl; for (int i = 0; i < TAMANO_ARREGLO; i++) cout << cadena[i] << endl; }//Fin de main() /* El siguiente programa: CADENA3.CPP, convierte una cadena de caracteres en minúsculas, mayúsculas e invierte la cadena. Si se introduce puras minúsculas o puras mayúsculas, el programa lo advierte. También advierte si las palabras son palíndromos. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strcpy(), strlwr(), strupr() const unsigned TAMANO_CADENA = 40; void main(void) { char str1[TAMANO_CADENA+1]; char str2[TAMANO_CADENA+1]; bool esMinuscula, esMayuscula, esSimetrico; cout << "Introduzca una cadena: "; cin.getline(str1, TAMANO_CADENA); cout << "\nLa cadena que introdujo fue: " << str1 << endl; strcpy(str2, str1); // Copia str1 a str2 strlwr(str2); // Convierte a minúsculas esMinuscula = (strcmp(str1, str2) == 0) ? true : false; cout << "\nMinúsculas: " << str2 << endl; strupr(str2); // Convierte a mayúsculas esMayuscula = (strcmp(str1, str2) == 0) ? true : false; cout << "Mayúsculas: " << str2 << endl << endl; strcpy(str2, str1); // Copia str1 a str2 strrev(str2); // Invierte los caracteres esSimetrico = (strcmp(str1, str2) == 0) ? true : false; cout << "Invertido: " << str2 << endl; if (esMinuscula) cout << "Su entrada no tiene letras mayúsculas" << endl; if (esMayuscula) cout << "Su entrada no tiene letras minúsculas" << endl; if (esSimetrico) cout << "Su entrada tiene caracteres simétricos" << endl; }//Fin de main() APUNTADORES Y CADENA – LECCIÓN 20 20-62 MIGUEL Á. TOLEDO MARTÍNEZ /* El siguiente programa: CADENA4.CPP, busca una subcadena dentro de una cadena e indica en donde comienza. Busca también un caracter indicando en que posición se encuentra. */ #include <iostream.h> #include <string.h> //Para cout y cin //Para strstr(), strchr const unsigned TAMANO_CADENA = 40; void main(void) { char cadenaPrincipal[TAMANO_CADENA+1]; char subCadena[TAMANO_CADENA+1]; char caracterBuscar; char *p; int indice; int contador; cout << "Introduzca una cadena : "; cin.getline(cadenaPrincipal, TAMANO_CADENA); cout << "Introduzca una subcadena de búsqueda: "; cin.getline(subCadena, TAMANO_CADENA); cout << "Introduzca un caracter de búsqueda : "; cin >> caracterBuscar; cout << endl; cout << " 1 2 3 4" << endl; cout << "01234567890123456789012345678901234567890" << endl; cout << cadenaPrincipal << endl << endl; cout << "Buscando la cadena " << subCadena << endl; p = strstr(cadenaPrincipal, subCadena); contador = 0; while (p) { contador++; indice = p - cadenaPrincipal; cout << "Localizado en el índice " << indice << endl; p = strstr(++p, subCadena); }//Fin de while if (contador == 0) cout << "No se localizó la subcadena en la cadena principal" << endl; cout << "\nBuscando el caracter " << caracterBuscar << endl; p = strchr(cadenaPrincipal, caracterBuscar); APUNTADORES Y CADENA – LECCIÓN 20 20-63 MIGUEL Á. TOLEDO MARTÍNEZ contador = 0; while (p) { contador++; indice = p - cadenaPrincipal; cout << "Localizado en el índice " << indice << endl; p = strchr(++p, caracterBuscar); }//Fin de while if (contador == 0) cout << "No localizado el caracter en la cadena principal" << endl; }//Fin de main() PENSANDO EN OBJETOS: Iteraciones entre los objetos Ésta es la última de las tareas de diseño orientado a objetos antes de iniciar el estudio de la programación orientada a objetos en otro curso. Después de completar esta tarea, estará preparado (y probablemente ansioso) para comenzar a codificar su simulador del elevador. Para completar el simulador del elevador como se definió, necesitará las técnicas de C++ de otro curso. En esta sección nos concentraremos en las interacciones entre los objetos. Esperamos que le sea de ayuda para armar el cuadro. Probablemente hará adiciones a la lista de objetos, atributos y comportamientos que antes fueron desarrollados. Hemos aprendido que la mayoría de los objetos en C++ no hacen las cosas de manera espontánea. En cambio, los objetos responden a estímulos, que vienen en forma de mensajes, los cuales de hecho son llamadas de función que invocan las funciones miembro de los objetos. Consideremos varias de las interacciones entre las clases de la simulación del elevador. El planteamiento del problema indica la persona oprime el botón que se encuentra en ese piso. El sujeto del enunciado es una persona y el objeto es el botón. Éste es un ejemplo de interacción entre clases. Un objeto de la clase persona envía un mensaje a un objeto botón. A ese mensaje lo llamamos oprimirBotón. Antes hemos hecho que el mensaje fuera una función miembro de la clase botón. En este punto, bajo Otros hechos de cada una de las clases de su simulación, todo lo que ha debido poner han sido las interacciones entre las clases. Algunas de ellas muestran explícitamente las interacciones entre los objetos de clase. Pero considere la oración "una persona espera a que se abra la puerta del elevador" Anteriormente hemos listados los dos comportamientos de la puerta del elevador, abrirPuerta y cerrarPuerta. Pero ahora queremos determinar qué clase de objetos envían estos mensajes. No se indican explícitamente en la oración entrecomillada anterior. Así que le damos algunas vueltas a esto y nos damos cuenta de que el elevador mismo envía el mensaje a la puerta. Estas interacciones entre los objetos de clase están implícitas en el planteamiento del problema. Ahora continúe refinando la sección Otros hechos de las distintas clases del simulador del elevador. Estas secciones ahora deberán contener principalmente las interacciones entre las clases. Vea cada una de ellas como: 1. 2. 3. un objeto de clase de envío particular que envía un mensaje particular a un objeto de clase de recepción particular Bajo cada clase, añada la sección Mensajes enviados a los objetos de otras clases (a tales mensajes se les llama colaboraciones; a partir de ahora emplearemos este término) y liste las interacciones entre las clases que quedan, es decir, bajo la clase persona, incluya la entrada : APUNTADORES Y CADENA – LECCIÓN 20 20-64 MIGUEL Á. TOLEDO MARTÍNEZ la persona le envía el mensaje oprimirBotón al botón de ese piso Bajo la clase botón, bajo Colaboraciones, coloque el mensaje: el botón le envía el mensaje venPorMí al elevador A medida que añada estas entradas, podrá agregar atributos y comportamientos a sus objetos. Esto es perfectamente natural. Cuando complete este ejercicio de laboratorio, tendrá una lista razonablemente completa de las clases que tendrá que implementar su simulador del elevador. Y por cada clase tendrá una lista razonablemente completa de sus atributos y comportamientos, así como de los mensajes que los objetos de dicha clase envían a los objetos de otras clases En el curso de la especialidad, estudiará la programación orientada a objetos en C++. Aprenderá cómo crear nuevas clases. Estará listo para escribir en C++ una parte importante del simulador de elevador. Conforme avance en sus conocimientos habrá aprendido lo suficiente para implementar un simulador operativo. Finalmente aprenderá a valerse de la herencia para explotar los puntos comunes entre las clases y minimizar la cantidad de software que necesitará escribir para resolver un problema. Hagamos un resumen del proceso de diseño orientado a objetos que hemos presentado: Escriba el planteamiento del problema en un archivo de texto. Descarte la información irrelevante. Extraiga todos los hechos. Disponga cada hecho en una línea independiente de un archivo de hechos. Busque los sustantivos en los hechos; con gran probabilidad serán muchas de las clases que necesitará. Por cada clase, poniéndola en primer nivel, escriba una lista de resumen. 5. Ponga cada hecho en el segundo nivel de la lista, bajo la clase apropiada. Si un hecho menciona varias clases (como sucederá en muchos casos), póngalo bajo todas ellas. 6. Ahora refine el conjunto de hechos bajo cada una de las clases. Liste tres subencabezados bajo cada clase: Atributos, Comportamientos y Colaboraciones. 7. Bajo Atributos liste la información asociada con cada clase. 8. Bajo Comportamientos liste las acciones que pueden llevar a cabo los objetos de dicha clase en respuesta a la recepción de un mensaje. Cada comportamiento es una función miembro de la clase. 9. Bajo Colaboraciones liste los mensajes que los objetos de esta clase envían a los objetos de otras clases y las clases que reciben estos mensajes. 10. En este punto su diseño probablemente tendrá todavía algunas piezas faltantes, que tal vez queden claras a medida que proceda con la implementación de su simulador en C++ conforme sus conocimientos de la programación orientada a objetos sean más amplios. 1. 2. 3. 4. Los atributos y comportamientos con frecuencia se llaman responsabilidades de una clase. La metodología de diseño que hemos perfilado aquí a veces se llama clases, responsabilidades y colaboraciones, o simplemente CRC. ERRORES COMUNES DE PROGRAMACIÓN 1. 2. 3. 4. 5. 6. Suponer que el * con el que se declara un apuntador se extiende a todos los nombres de variables de una lista de variables de apuntador separada por comas puede causar que tales apuntadores se declaren como no apuntadores. Cada apuntador debe declararse con un * como prefijo del nombre. Desreferenciar un apuntador que no ha sido inicializado de manera apropiada o al que no se le ha indica- do que apunte a una localidad específica de memoria, puede producir un error fatal de tiempo de ejecución o modificar accidentalmente información importante, permitiendo que el programa se ejecute hasta el final, lo que arrojará resultados incorrectos. El intento por desreferenciar un no apuntador es un error de sintaxis. Desreferenciar un apuntador 0 por lo general causa un error fatal en tiempo de ejecución. Es un error no desreferenciar un apuntador cuando es necesario hacerlo con el fin de obtener el valor al que apunta el apuntador. No inicializar un apuntador que se ha declarado corno const es un error de sintaxis. APUNTADORES Y CADENA – LECCIÓN 20 20-65 MIGUEL Á. TOLEDO MARTÍNEZ 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. El empleo del operador sizeof en una función con el fin de determinar el tamaño en bytes de un parámetro de arreglo da como resultado el tamaño en bytes del apuntador, no el tamaño en bytes del arreglo. La omisión de los paréntesis en una operación sizeof cuando el operando es un nombre de tipo es un error de sintaxis. La utilización de aritmética de apuntadores sobre un apuntador que no hace referencia a un arreglo de valores normalmente es un error de lógica. La resta o comparación de dos apuntadores que no hacen referencia a elementos del mismo arreglo por lo general es un error de lógica. Salirse de cualquiera de los extremos de un arreglo al aplicar aritmética de apuntadores normalmente es un error de lógica. La asignación de un apuntador de un tipo a otro apuntador de un tipo distinto (que no sea void *) sin convertir el primer apuntador al tipo del segundo es un error de sintaxis. Desreferenciar un apuntador void * es un error de sintaxis. Aunque los nombre de arreglos son apuntadores al inicio de éstos y aunque los apuntadores se pueden modificar con expresiones aritméticas, los nombres de los arreglos no pueden modificarse mediante expresiones aritméticas, dado que son apuntadores constantes. Es un error no asignar suficiente espacio en un arreglo de caracteres para almacenar el carácter nulo de terminación de la cadena. Es un error crear o utilizar una “cadena” que no contenga un carácter nulo de terminación. El procesamiento de un solo carácter como cadena puede generar un error fatal en tiempo de ejecución. Una cadena es un apuntador -probablemente sea un entero de tamaño respetablemente grande. Sin embargo, un carácter es un entero pequeño (los valores ASCII van de 0 a 255) En muchos sistemas, esto provoca un error, pues las direcciones de memoria bajas se reservan para fines especiales, como controladores de interrupciones del sistema operativo -por lo tanto, suceden “violaciones de acceso”. Si pasa un carácter como argumento a una función cuando se espera una cadena, puede causar un error fatal en tiempo de ejecución. Pasar una cadena como argumento de una función cuando se espera un carácter es un error de sintaxis. Es un error olvidar incluir el archivo de encabezado <string.h> cuando se emplean funciones de la biblioteca de manejo de cadenas. No agregar un carácter nulo de terminación al primer argumento de strncpy cuando el tercer argumento es menor o igual que la longitud de la cadena del segundo argumento puede ser causa de errores fatales en tiempo de ejecución. La suposición de que strcmp y strncmp devuelven 1 cuando sus argumentos son iguales es un error de lógica. Ambas funciones devuelven 0 (que es el valor de falso de C++) cuando hay una igualdad. Por lo tanto, al probar la igualdad de dos cadenas, el resultado de la función strcmp o strncmp debe compararse con 0 para determinar si son iguales. Es un error no notar que strtok modifica la cadena que se está dividiendo en tokens y luego intentar utilizarla como si fuera la cadena original, sin modificaciones. BUENAS PRÁCTICAS DE PROGRAMACIÓN 1. 2. 3. 4. 5. Aunque no se requiere, la inclusión de las letras Ptr en los nombres de variables de apuntador deja claro que dichas variables son apuntadores y que necesitan manipularse como tales. Utilice una llamada por valor para pasar argumentos a una función, a menos que el invocador requiera explícitamente que la función llamada modifique el valor de la variable argumento en el entorno del invocador. Éste es otro ejemplo del principio de menor privilegio. Antes de utilizar una función, revise su prototipo de función a fin de determinar los parámetros que puede modificar. Utilice la notación de arreglos en lugar de la notación de apuntadores cuando manipule arreglos. Aunque la compilación del programa tal vez tarde un poco más, con seguridad será más claro. Al almacenar una cadena de caracteres en un arreglo de caracteres, debe asegurarse de que el arreglo es lo bastante grande para contener la cadena más grande que almacenará. C++ permite el almacenamiento de cadenas de cualquier longitud. Si una cadena es más grande que el arreglo en el que la almacenará, los caracteres que estén después del final del arreglo sobrescribirán la información en memoria que se encuentre a continuación de dicho arreglo. APUNTADORES Y CADENA – LECCIÓN 20 20-66 MIGUEL Á. TOLEDO MARTÍNEZ PROPUESTAS DE DESEMPEÑO 1. 2. 3. Los objetos grandes, como las estructuras, se deben pasar mediante apuntadores hacia datos constantes o con referencias hacia datos constantes, con el fin de lograr los beneficios en desempeño de la llamada por referencia y la seguridad de la llamada por valor. sizeof es un operador unario de tiempo de compilación, no una función de tiempo de ejecución. Por lo tanto, la utilización de sizeof no afecta negativamente el desempeño en tiempo de ejecución. A veces los algoritmos que emergen de manera natural pueden tener sutiles problemas de desempeño, como el aplazamiento indefinido. Es importante buscar algoritmos que eviten el aplazamiento indefinido. PROPUESTAS DE PORTABILIDAD 1. 2. 3. 4. 5. 6. 7. El formato en el que se envía a la salida un apuntador depende de la máquina. Algunos sistemas envían a la salida los valores de apuntador corno enteros hexadecimales, mientras que otros lo hacen como enteros decimales. Aunque const está bien definido en el ANSI C y C++, algunos compiladores no lo aplican adecuadamente. El número de bytes que se utilizan para almacenar un tipo de datos particular puede variar según el sistema. Al escribir programas que dependan de los tamaños de los tipos de datos y que se ejecutarán en varios sistemas de cómputo, hay que utilizar sizeof para determinar el número de bytes en que se almacenan los distintos tipos de datos. La mayoría de las computadoras actuales tienen enteros de 2 o 4 bytes. Algunas de las más nuevas utilizan enteros de 8 bytes. Debido a que los resultados de la aritmética de apuntadores dependen del tamaño de los objetos a los que apunta un apuntador, la aritmética de apuntadores depende de la máquina. Cuando se inicializa una variable de tipo char * con una literal de cadena, algunos compiladores pueden poner dicha cadena en alguna localidad de memoria en la que no puede ser modificado. Si necesitara modificar una literal de cadena, deberá almacenarla en un arreglo de caracteres, a fin de asegurar la posibilidad de modificarla en todos los sistemas. Los códigos numéricos empleados para la representación de caracteres pueden cambiar según la computadora. No use los códigos ASCII específicos, como en el caso de if ( ch == 65 ) más bien, emplee la constante del carácter, como en if ( ch = = ‘A’ ). OBSERVACIONES DE INGENIERÍA DE SOFTWARE 1. 2. 3. 4. 5. El calificador const puede servir para aplicar el principio de menor privilegio. La utilización del principio de menor privilegio para diseñar software de la manera correcta puede reducir en gran medida el tiempo de depuración y los efectos secundarios, así como simplificar la modificación y el mantenimiento de los programas. Si un valor no cambia (o no debe cambiar) en el cuerpo de una función a la cual se pasa, el parámetro debe ser declarado como const con el fin de asegurar que no sea modificado por accidente. Sólo es posible alterar un valor en un invocador cuando se efectúa una llamada por valor. Este valor debe asignarse a partir del valor de devolución de la función. Si hay que modificar varios valores en un invocador, es necesario pasar varios argumentos mediante llamada por referencia. La colocación de prototipos de función en las definiciones de otras funciones aplica el principio de menor privilegio, restringiendo las llamadas de función a las funciones en las que aparecen los prototipos. Al pasar un arreglo a una función, también hay que pasar su tamaño (en lugar de integrarlo en la función) Esto contribuye a hacer más general la función. Las funciones generales con frecuencia son reutilizables en otros programas. INDICACIONES DE PRUEBA Y DEPURACIÓN 1. Los apuntadores se deben inicializar con el fin de evitar que apunten a áreas desconocidas o no inicializadas de la memoria. APUNTADORES Y CADENA – LECCIÓN 20 20-67 MIGUEL Á. TOLEDO MARTÍNEZ LO QUE NECESITA SABER Antes de continuar con la siguiente lección, asegúrese de haber aprendido lo siguiente: !"Los apuntadores son variables que contienen como valor la dirección de otras variables. !"La declaración: int *ptr; declara que ptr es un apuntador a un objeto de tipo int, y se lee ptr es un apuntador a int. El * como se emplea en una declaración, indica que la variable es un apuntador. !"Hay tres valores que se pueden utilizar para inicializar un apuntador: 0, NULL o la dirección de un objeto del mismo tipo. Es lo mismo inicializar un apuntador a 0 que a NULL. !"El único entero que se puede asignar a un apuntador es 0. !"El operador & (de dirección) devuelve la dirección de su operando. !"El operando del operador de dirección debe ser un nombre de variable; el operador de dirección no se puede aplicar a constantes, a expresiones que no devuelvan una referencia ni a variables declaradas con la clase de almacenamiento register. !"El operador *, conocido como operador de indirección o de desreferencia, devuelve un sinónimo, alias o apodo del nombre del objeto al que apunta su operando en memoria. A esto se le llama desreferenciar el apuntador. !"Al llamar una función con un argumento que el invocador desea que la función llamada modifique, debe pasarse la dirección del argumento. La función llamada modifica entonces el valor del argumento del invocador mediante el operador de indirección (*) !"Una función que recibe como argumento una dirección debe incluir un apuntador corno su parámetro correspondiente. !"No es necesario incluir los nombres de los apuntadores en los prototipos de función; sólo es necesario incluir los tipos de apuntador. Los nombres de apuntador se pueden agregar con fines de documentación, pero el compilador los ignora. !"El calificador const le permite al programador informarle al compilador que no se deberá modificar el valor de una variable en particular. !"Si se intenta modificar un valor const, el compilador lo detecta y emite un aviso o un error, según el compilador de que se trate. !"Hay cuatro maneras de pasar un apuntador a una función: mediante un apuntador no constante hacia datos no constantes, por medio de un apuntador no constante hacia datos constantes, utilizando un apuntador constante hacia datos no constantes y a través de un apuntador constante hacia datos constantes. !"Los arreglos se pasan de manera automática por referencia, utilizando apuntadores, pues el valor del nombre del arreglo es la dirección del mismo. !"Para pasar un solo elemento de un arreglo como llamada por referencia mediante apuntadores, es necesario pasar la dirección del elemento específico del arreglo. !"C++ ofrece el operador unario especial sizeof, que determina el tamaño de un arreglo (o de cualquier otro tipo de datos) en bytes durante la compilación del programa. !"Cuando se aplica al nombre de un arreglo, el operador sizeof devuelve, como entero, la cantidad total de bytes que hay en ella. !"El operador sizeof se puede aplicar a cualquier nombre de variable, tipo o constante. !"Las operaciones aritméticas que se pueden llevar a cabo sobre los apuntadores son incremento (++), decremento (--), suma (+ o + =) o resta (- o - =) de un apuntador y un entero, y resta de un apuntador a otro. !"Cuando se suma o resta un entero a un apuntador, el apuntador se incremento o decrementa por dicho entero, multiplicado por el tamaño del objeto al que se apunta. !"Las operaciones de aritmética de apuntadores sólo deben efectuarse sobre partes contiguas de la memoria, por ejemplo, sobre un arreglo. Todos los elementos de un arreglo se almacenan en localidades contiguas de memoria. APUNTADORES Y CADENA – LECCIÓN 20 20-68 MIGUEL Á. TOLEDO MARTÍNEZ !"Al llevar a cabo aritmética de apuntadores sobre un arreglo de caracteres, el resultado es como el de la aritmética normal, pues cada carácter se almacena en un byte de memoria. !"Se puede asignar un apuntador a otro si ambos son del mismo tipo. De otra manera, debe aplicarse una conversión mediante cast. La excepción a esto es un apuntador a void, que es un tipo de apuntador general que puede contener apuntadores de cualquier tipo. A los apuntadores a void se les puede asignar apuntadores de otros tipos. Es posible asignar un apuntador void a un apuntador de otro tipo sólo a través de una conversión mediante cast explícita. !"No se puede desreferenciar un apuntador a void. !"Los apuntadores se pueden comparar por medio de los operadores de igualdad y relacionales. Las comparaciones de apuntadores por lo general sólo tienen sentido si los apuntadores apuntan a miembros del mismo arreglo. !"Los apuntadores se pueden indexar de la misma manera que los nombres de arreglos. !"Un nombre de arreglo es equivalente a un apuntador al primer elemento del mismo. !"En la notación de apuntador/desplazamiento, el desplazamiento es igual a un índice de arreglo. !"Todas las expresiones de arreglos indexadas se pueden escribir con un apuntador y un desplazamiento, utilizando corno apuntador el nombre del arreglo o un apuntador separado que apunte a dicho arreglo. !"Un nombre de arreglo es un apuntador constante que siempre apunta a la misma localidad de memoria. !"Es posible tener arreglos de apuntadores. !"Un apuntador a una función es la dirección donde reside el código de la función. !"Los apuntadores a funciones pueden ser pasados a otras funciones, devueltos de otras funciones, almacenados en arreglos y asignados a otros apuntadores. !"Un uso común de los apuntadores de función es en los llamados sistemas operados por menú. Los apuntadores de función sirven para seleccionar la función a llamar según un elemento particular del menú. !"La función strcpy copia su segundo argumento (una cadena) a su primer argumento (un arreglo de caracteres) El programador debe asegurarse de que el arreglo es lo bastante grande para almacenar la cadena y su carácter nulo de terminación. !"La función strncpy es equivalente a strcpy, excepto que una llamada a strncpy especifica el número de caracteres a copiar de la cadena al arreglo. El carácter nulo de terminación sólo se copiará si el número de caracteres a copiar es de uno más que la longitud de la cadena. !"La función strcat añade la cadena de su segundo argumento (incluyendo el carácter nulo de terminación) a la cadena de su primer argumento. El primer carácter de la segunda cadena reemplaza el carácter nulo ( ‘\0’ ) de la primera cadena. El programador debe asegurarse de que el arreglo en el que está la primera cadena es lo bastante grande para almacenar tanto la primera cadena como la segunda. !"La función strncat agrega un número de caracteres especificado de la segunda cadena a la primera. Se añade un carácter nulo de terminación al resultado. !"La función strcmp compara la cadena de su primer argumento con la cadena de su segundo argumento, carácter por carácter. La función devuelve 0 si ambas son iguales, un resultado negativo si la primera es menor que la segunda y un valor positivo si la primera es mayor que la segunda. !"La función strncmp es equivalente a strcmp, excepto que strncmp compara un número especificado de caracteres. Si el número de caracteres de una cadena es menor que el número especificado, strncmp compara los caracteres hasta que se encuentra el carácter nulo en la cadena más corta. !"Una secuencia de llamadas a strtok divide una cadena en tokens separados por los caracteres contenidos en el segundo argumento. La primera llamada contiene como primer argumento la cadena a dividir en tokens, y las llamadas siguientes que continúan dividiendo la misma cadena en tokens contienen NULL como primer argumento. Cada llamada devuelve un apuntador al token actual. Si no hay más tokens al llamar a strtok, se devuelve NULL. !"La función strlen toma como argumento una cadena y devuelve el número de caracteres que hay en ella; en la longitud de la cadena no se incluye el carácter nulo de terminación. !"Una cadena de caracteres es un arreglo de caracteres que terminan por los caracteres ASCII 0 o NULL. !"Puede crear una cadena de caracteres dentro de su programa mediante la declaración de un arreglo de tipo char. !"Su programa es responsable de agregar el carácter NULL después del último carácter de la cadena. APUNTADORES Y CADENA – LECCIÓN 20 20-69 MIGUEL Á. TOLEDO MARTÍNEZ !"Cuando su programa utiliza cadenas constantes entre comillas, el compilador C++ automáticamente agrega el carácter NULL. !"C++ le permite inicializar las cadenas al declararlas, especificando los caracteres deseados entre comillas. !"La mayoría de los compiladores C++ proporcionan un conjunto de funciones para el manejo de cadenas en las librerías en tiempo de ejecución. PREGUNTAS Y PROBLEMAS PREGUNTAS 1. Llene los siguientes espacios en blanco: a) Un apuntador es una variable que contiene como valor la ______________ de otra variable. b) Los tres valores que se pueden utilizar para inicializar un apuntador son __________, _________ y ____________. c) El único entero que se puede asignar a un apuntador es________________ 2. Indique si las siguientes oraciones son verdaderas o falsas. Si la respuesta es falsa, explique por qué. a) El operador de dirección & sólo puede aplicarse a constantes, a expresiones y a variables declaradas con la clase de almacenamiento register. b) Se puede desreferenciar un operador declarado como void. c) No es posible asignar un apuntador de un tipo a otro de otro tipo sin una operación de conversión mediante cast. 3. Responda las siguientes preguntas. Suponga que los números de punto flotante de precisión sencilla se almacenan en 4 bytes y que la dirección de inicio del arreglo está en la localidad 1002500 de la memoria. Cada parte del ejercicio debería utilizar el resultado de las partes previas, donde aplique. a) Declare un arreglo de tipo float llamado numeros que tenga 10 elementos, los cuales deberá inicializar a los valores 0.0, 1.1, 2.2, ..., 9.9. Suponga que la constante simbólica TAMANO se ha definido como10. b) Declare el apuntador nPtr que apunta a un objeto de tipo float. c) Imprima los elementos del arreglo numeros utilizando la notación de índices de arreglos. Emplee una estructura for y suponga que se ha declarado la variable de control entera i. Imprima cada número con una posición de precisión a la derecha del punto decimal. d) Dé dos instrucciones independientes que asignen la dirección de inicio del arreglo numeros a la variable de apuntador nPtr. e) Imprima los elementos del arreglo numeros por medio de notación de apuntador/desplazamiento con el apuntador nPtr. f) Imprima los elementos del arreglo numeros utilizando notación de apuntador/desplazamiento con el nombre del arreglo como apuntador. g) Imprima los elementos del arreglo numeros indexando el apuntador nPtr. h) Haga referencia al elemento 4 del arreglo numeros empleando notación de índices de arreglos, notación de apuntador/desplazamiento con el nombre del arreglo como apuntador, notación de índice de apuntador con nPtr y notación de apuntador/desplazamiento con nPtr. i) Suponiendo que nPtr apuntara al inicio del arreglo numeros, ¿a qué dirección hace referencia nPtr + 8? ¿Qué valor se almacena en dicha localidad? j) Suponiendo que nPtr apunte a numeros[5], ¿qué dirección referencia nPtr después de que se ejecuta nPtr -= 4?. ¿Qué valor se almacena en esa localidad? 4. Escriba una sola instrucción que lleve a cabo la tarea indicada para cada uno de los siguientes casos. Suponga que ya se han declarado las variables de punto flotante numero1 y numero2, y que numero1 se ha inicializado a 7.3. También suponga que la variable ptr es de tipo char * y que los arreglos s1[100] s2[100] son de tipo char. a) b) c) d) Declare la variable fPtr como apuntador a un objeto de tipo float. Asígnele a la variable de apuntador fPtr la dirección de la variable numero1. Imprima el valor del objeto al que apunta fPtr. Asígnele a la variable numero2 el valor del objeto al que apunta fPtr. APUNTADORES Y CADENA – LECCIÓN 20 20-70 MIGUEL Á. TOLEDO MARTÍNEZ e) f) g) h) i) j) k) l) 5. Imprima el valor de numero2. Imprima la dirección de numero1. Imprima la dirección almacenada en fPtr. ¿El valor que se imprime es igual a la dirección de numero1? Copie la cadena almacenada en el arreglo s2 al arreglo s1. Compare la cadena en s1 con la cadena en s2. Imprima el resultado. Agregue 10 caracteres de la cadena de s2 a la cadena de s1. Determine la longitud de la cadena de s1. Imprima el resultado. Asígnele a ptr la localidad del primer token de s2. Los tokens de s2 se separan mediante comas (,). Haga lo siguiente: a) Escriba el encabezado de una función llamada intercambio(), que toma como parámetros los apuntadores a los números de punto flotante x e y y que no devuelve nada. b) Escriba el prototipo de la función de la parte (a). c) Escriba el encabezado de una función llamada evaluar(), que devuelve un entero y toma como parámetros el entero x y un apuntador a la función poly. Ésta toma un parámetro entero y devuelve un entero. d) Escriba el prototipo de función de la parte (c) e) Muestre dos métodos diferentes para inicializar el arreglo de caracteres vocal con la cadena de vocales “AEIOU”. 6. Encuentre los errores en los siguientes segmentos de programa. Suponga que int *zPtr; // zPtr referenciará el arreglo z int *aPtr = 0; void *sPtr = 0; int numero, i; int z[5] = { 1, 2, 3, 4, 5} sPtr = z; a) ++zPtr; b) // Utiliza el apuntador para obtener el primer valor del arreglo numero = zPtr; c) // le asigna a numero el elemento 2 del arreglo (el valor 3) a. numero = *zPtr[ 2]; d) // imprime todo el arreglo z a. for ( i = 0; i <= 5; i++ ) i. cout << zPtr[ i ] << endl; e) // le asigna a numero el valor al que apunta sPtr f) numero = *sPtr; g) ++z; h) char s[10]; i) cout << strncpy( s, “adios”, 5) << endl; j) char s[12]; k) strcpy( s, “Bienvenido a casa”); l) if( strcmp( cadena1, cadena2)) cout << “Las cadenas son iguales” << endl; 7. ¿Qué imprimen las siguientes instrucciones cuando se ejecutan (si es que imprimen algo)? Si alguna de estas contiene un error, descríbalo e indique la manera de corregirlo. Suponga las siguientes declaraciones de variables: char s1[50] = “jack”, s2[50] = “jill”, s3[50], *sptr; a) b) c) d) cout << strcpy(s3, s2) << endl; cout << strcat(strcat(strcpy(s3, s1), “ and “ ), s2) << endl; cout << strlen(s1) + strlen(s2) << endl; cout << strlen(s3) << endl; APUNTADORES Y CADENA – LECCIÓN 20 20-71 MIGUEL Á. TOLEDO MARTÍNEZ 8. Indique si las siguientes oraciones son verdaderas o falsas. Si la respuesta es falsa, explique por qué. a) La comparación de dos apuntadores que apuntan a arreglos diferentes no tiene sentido. b) Debido a que el nombre de un arreglo es un apuntador al primer elemento de dicho arreglo, los nombres de arreglo deben manipularse de la misma manera que los apuntadores. 9. Conteste a lo siguiente. Suponga que los enteros sin signo se almacenan en 2 bytes y que la dirección inicial del arreglo es la localidad 1002500 de la memoria. a) Declare un arreglo de tipo unsigned int llamado valores que cuente con 5 elementos, los cuales deberá inicializar a los enteros pares del 2 al 10. Suponga que la constante simbólica se ha definido como 5. b) Declare el apuntador vPtr que apunta a un objeto de tipo unsigned int. c) Imprima los elementos del arreglo valores utilizando notación de índices de arreglo. Emplee una estructura for y suponga que se ha declarado la variable de control entera i. d) Escriba dos instrucciones separadas que asignen la dirección inicial del arreglo valores a la variable de apuntador vPtr. e) Imprima los elementos del arreglo valores utilizando notación de apuntador/desplazamiento. f) Imprima los elementos del arreglo valores utilizando notación de apuntador/desplazamiento con el nombre del arreglo como apuntador. g) Imprima los elementos del arreglo valores indexando el apuntador al arreglo. h) Haga referencia al elemento 5 de valores utilizando notación de índices de arreglo, notación de apuntador/desplazamiento con el nombre del arreglo como apuntador, notación de índices de apuntador y notación de apuntador/desplazamiento. i) ¿A qué dirección hace referencia vPtr + 3? ¿Qué valor se almacena en dicha localidad? j) Suponiendo que vPtr apunte a valores[4], ¿a qué dirección hace referencia vPtr -= 4? ¿Qué valor se almacena en dicha localidad? 10. Para cada uno de los siguientes puntos, escriba una instrucción que lleve a cabo la tarea indicada. Suponga que se han declarado las variables enteras largas valor1 y valor2, y que valor1 se ha inicializado a 200000. a) b) c) d) e) f) g) Declare la variable lPtr como apuntador a un objeto de tipo long. Asigne la dirección de la variable valor1 a la variable de apuntador lPtr. Imprima el valor del objeto al que apunta lPtr. Asígnele a la variable valor2 el valor del objeto al que apunta lPtr. Imprima el valor de valor2. Imprima la dirección de valor1. Imprima la dirección almacenada en lPtr. ¿Es igual el valor impreso que la dirección de valor1? 11. Haga lo siguiente. a) Escriba el encabezado de la función cero, que toma como parámetro el arreglo de enteros largos enterosLargos y no devuelve nada. b) Escriba el prototipo de la función de la parte (a) c) Escriba el encabezado de la función agrega1YSuma, que toma como parámetro el arreglo de enteros demasiadoPequeno y devuelve un entero. d) Escriba el prototipo de la función descrita en la parte (c) APUNTADORES Y CADENA – LECCIÓN 20 20-72 MIGUEL Á. TOLEDO MARTÍNEZ PROBLEMAS Nota: Los problemas 1 a 4 son algo complejos. Una vez que los haya hecho, debería poder implementar con facilidad los juegos de naipes más comunes. 1. Modifique el programa BARAJAS.CPP, para que la función de barajado de los naipes reparta una mano de póquer de cinco naipes. Después escriba las funciones que realicen lo siguiente: a) b) c) d) e) f) Determine si la mano contiene un par. Determine si la mano contiene dos pares. Determine si la mano contiene una tercia (por ejemplo, tres sotas) Determine si la mano contiene un póquer (por ejemplo, cuatro ases) Determine si la mano contiene un flux (es decir, los cinco naipes del mismo palo) Determine si la mano contiene una corrida (es decir, cinco naipes del mismo palo con valores consecutivos) 2. Utilice las funciones desarrolladas en el problema 1 para escribir un programa que reparta dos manos de póquer de cinco naipes, las evalúe y determine cuál es la mejor. 3. Modifique el problema desarrollado en el problema 2 de modo que simule al repartidor. La mano del repartidor se baraja cerrada, para que el jugador no la pueda ver. El programa deberá evaluar dicha mano y, con base en su calidad, cambiar las inservibles de la mano original por uno, dos o tres naipes. El programa deberá reevaluar la mano del repartidor. (Precaución: éste es un problema complicado) 4. Modifique el programa desarrollado en el problema 3 para que pueda manejar automáticamente la mano del repartidor, pero que le permita al jugador decidir cuáles de los naipes de su propia mano cambiará. El programa deberá evaluar ambas manos y decidir quién gana. Ahora utilice este nuevo programa para jugar 20 juegos contra la computadora. ¿Quién gana más juegos, usted o la computadora? Haga que uno de sus amigos juegue 20 juegos contra la computadora. ¿Quién gana más juegos? Con base en estos resultados, haga las modificaciones necesarias para refinar el programa de póquer (éste también es un problema difícil) Juegue 20 juegos más. ¿El programa modificado juega mejor? 5. En el programa de barajado y repartición de naipes BARAJAS.CPP, utilizamos intencionalmente un algoritmo de barajado ineficiente que presentó la posibilidad de aplazamiento indefinido. En este problema, creará un algoritmo de barajado de alto desempeño que evite el aplazamiento indefinido. Modifique el programa BARAJAS.CPP, como sigue. Inicialice el arreglo paquete como se muestra en la figura 20.10 . Modifique la función barajar() para que haga un ciclo fila por fila y columna por columna a través del arreglo, tocando una vez todos los elementos. Cada elemento debe intercambiarse con otro elemento del arreglo seleccionado al azar. Imprima el arreglo resultante para determinar si se barajó de manera adecuada (como en la figura 20.11, por ejemplo) Tal vez desee que el programa llame varias veces a la función barajar(), a fin de asegurar un barajado satisfactorio. Note que, aunque este enfoque mejor el algoritmo de barajado, éste aún tiene que buscar en el arreglo paquete el naipe 1, luego el 2, el 3, etc. Es más, aún después de que el algoritmo de barajado encuentra y reparte el naipe, sigue buscando en el resto de los naipes. Modifique el programa BARAJAS.CPP para que, una vez repartido un naipe, no se hagan mas intentos por encontrarlo, procediendo de inmediato a repartir el siguiente. Arreglo paquete sin barajar 0 1 2 3 0 1 14 27 40 1 2 15 28 41 2 3 16 29 42 3 4 17 30 43 4 5 18 31 44 5 6 19 32 45 6 7 20 33 46 7 8 21 34 47 8 9 22 35 48 9 10 23 36 49 10 11 24 37 50 11 12 25 38 51 12 13 26 39 52 Figura 20.10. Arreglo paquete sin barajar APUNTADORES Y CADENA – LECCIÓN 20 20-73 MIGUEL Á. TOLEDO MARTÍNEZ Ejemplo del arreglo paquete sin barajado 0 1 2 3 0 19 13 12 50 1 40 28 33 38 2 27 14 15 52 3 25 16 42 39 4 36 21 43 48 5 46 30 23 51 6 10 8 45 9 7 34 11 3 5 8 35 31 29 37 9 41 17 32 49 10 18 24 4 22 11 2 7 47 6 12 44 1 26 20 Figura 20.11. Ejemplo del arreglo paquete sin barajado 6. (Simulación: la tortuga y la liebre) En este problema, recreará la carrera clásica de la tortuga y la liebre. Se valdrá de la generación de números aleatorios para desarrollar la simulación de este memorable evento. Nuestros contendientes comienzan la carrera en el cuadro 1 de una serie de 70 cuadros. Cada cuadro representa una posición posible en la ruta de la carrera. La línea de meta está en el cuadro 70. El primer contendiente que llegue o pase el cuadro 70 obtiene como recompensa un cubo de zanahorias y lechuga fresca. La ruta sube serpenteando por la ladera de una montaña resbalosa, por lo que ocasionalmente los contendientes pierden terreno. Hay un reloj que pulsa una vez por segundo. Con cada pulso del reloj, el programa deberá ajustar la posición de los animales, de acuerdo con las siguientes reglas: Animal Tipo de movimiento Tortuga Paso veloz Resbalón Paso lento Liebre Duerme Gran salto Gran resbalón Salto pequeño Resbalón pequeño Porcentaje del tiempo 50% 20% 30% 20% 20% 10% 30% 20% Movimiento real 3 cuadros a la derecha 6 cuadros a la izquierda 1 cuadro a la derecha No se mueve 9 cuadros a la derecha 12 cuadros a la izquierda 1 cuadro a la derecha 2 cuadros a la izquierda Utilice variables para llevar el registro de las posiciones de los animales (las posiciones son de la 1 a la 70) Cada animal debe comenzar en la posición 1 (es decir, la puerta de salida) Si un animal resbala hacia la izquierda, quedando antes del cuadro 1, devuélvalo al cuadro 1. Genere los porcentajes de la tabla previa produciendo un entero aleatorio, i, que esté en el rango 1 ≤ i ≤ 10. Para la tortuga, efectúe un paso veloz cuando 1 ≤ i ≤ 5, un resbalón cuando 6 ≤ i ≤ 7 y un paso lento cuando 8 ≤ i ≤ 10. Utilice una técnica similar para mover a la liebre. Comience la carrera imprimiendo ¡BANG! ¡Y ARRANCAN! Por cada pulso del reloj (es decir, cada repetición del ciclo), imprima una línea de 70 posiciones que muestre la posición de la tortuga mediante la letra T y la de la liebre mediante la letra L. Ocasionalmente los contendientes caerán en el mismo cuadro. En este caso, la tortuga morderá a la liebre y el programa deberá imprimir ¡OUCH! en tal posición. Todas las posiciones de impresión que no sean T, L ni ¡OUCH! (en caso de empate), deben estar en blanco. Después de la impresión de cada línea, pruebe si alguno de los animales ha llegado o pasado el cuadro 70. De ser así, imprima el ganador y termine la simulación. Si la tortuga gana, imprima ¡LA TORTUGA GANA! ¡BRAVO! Si la liebre gana, imprima La liebre gana. Ni hablar. Si ambos animales llegan a la meta con el mismo pulso de reloj, tal vez usted quiera favorecer a la tortuga (el desvalido) o imprimir Es un empate. Si ninguno de los animales gana, vuelva a efectuar el ciclo, simulando el siguiente pulso del reloj. APUNTADORES Y CADENA – LECCIÓN 20 20-74 MIGUEL Á. TOLEDO MARTÍNEZ SECCIÓN ESPECIAL: Construya su propia computadora En los siguientes problemas nos desviaremos temporalmente del mundo de la programación con lenguajes de alto nivel. Abriremos una computadora y veremos su estructura interna. Presentaremos la programación en lenguaje de máquina y escribiremos varios programas en dicho lenguaje. Para hacer que esta sea una experiencia especialmente valiosa, después construiremos una computadora (por medio de la técnica de simulación basada en software) en la que podrá ejecutar sus programas en lenguaje de máquina. 7. (Programación en lenguaje de máquina) Crearemos una computadora a la que llamaremos Simpletron. Como su nombre implica, es una máquina sencilla, pero como pronto veremos, también es poderosa. La Simpletron ejecuta programas escritos en el único lenguaje que entiende directamente; es decir, el lenguaje de máquina Simpletron, o SML. La Simpletron contiene un acumulador -es decir, un registro especial en el que se coloca la información antes de que la Simpletron la tome para efectuar cálculos o examinarla de varias maneras. Toda la informa de la Simpletron se maneja en términos de palabras. Una palabra es un número decimal de cuatro dígitos con signo, como +3364, -1293, +0007, -0001, etc. La Simpletron viene equipada con una memoria de 100 palabras, las cuales se referencian por su número de localidad: 00, 01, ..., 99. Antes de ejecutar un programa SML, debemos cargar el programa en memoria. La primera instrucción de cada programa SML siempre queda en la localidad 00. El simulador comenzará la ejecución a partir de localidad. Cada instrucción en SML, ocupa una palabra de la memoria de la Simpletron (por lo tanto, las instrucciones son números decimales de cuatro dígitos con un signo) Supondremos que el signo de una instrucción SML siempre es positivo, pero el signo de una palabra de datos puede ser positivo o negativo. Cada localidad de la memoria de la Simpletron puede contener una instrucción, un valor de datos utilizado por el programa o un área no utilizada (y, por lo tanto, indefinida) de la memoria. Los primeros dos dígitos de cada instrucción SML son el código de operación, que especifica que se llevará a cabo. Los códigos de operación de SML aparecen en la figura 20.12. Código de operación Significado Operaciones de entrada/salida: const int READ = 10; Lee a palabra del teclado y la pone en una localidad específica de la memoria const int WRITE = 11; Escribe en la pantalla la palabra ubicada en una localidad específica de memoria. Operaciones de carga/almacenamiento: const int LOAD = 20; Carga en el acumulador la palabra ubicada en una localidad específica de memoria. Const int STORE = 21; Almacena la palabra que se encuentra en el acumulador en una localidad específica de memoria. Operaciones aritméticas: const int ADD = 30; Suma la palabra ubicada en una localidad específica de memoria a la palabra que se encuentra en el acumulador (deja el resultado en el acumulador). const int SUBTRACT = 31; Resta la palabra ubicada en una localidad específica de memoria de la palabra que se encuentra en el acumulador (deja el resultado en el acumulador). const int DIVIDE = 32; Divide la palabra ubicada en una localidad específica de memoria entre la palabra que se encuentra en el acumulador (deja el resultado en el acumulador). const int MULTIPLY = 33; Multiplica la palabra ubicada en una localidad específica de memoria por la palabra que se encuentra APUNTADORES Y CADENA – LECCIÓN 20 20-75 MIGUEL Á. TOLEDO MARTÍNEZ Código de operación Significado en el acumulador (deja el resultado en el acumulador). operaciones de transferencia de control: const int BRANCH = 40; Bifurca a una localidad específica de memoria. const int BRANCHNEG = 41; Bifurca a una localidad específica de memoria si el acumulador es negativo. const int BRANCHZERO = 41; Bifurca a una localidad específica de memoria si el acumulador es igual a cero. const int HALT = 43; Termina, el programa ha completado su trabajo. Figura 20.12. Códigos de Operación del lenguaje de máquina Simpletron Los últimos dos dígitos de las instrucciones SML son el operando (la dirección de la localidad específica de memoria que contiene la palabra a la que se aplica la operación) Ahora consideremos varios programas SML sencillos. El primero (ejemplo 1) lee del teclado dos números y calcula e imprime su suma. La instrucción +1007 lee el primer número y lo pone en la localidad 07 (la cual ha sido inicializada a cero) La instrucción +1008 lee el siguiente número, colocándolo en la localidad 08. La instrucción load, +2007, pone (copia) en el acumulador el primer número y la instrucción add, +3008, suma el segundo número al que se encuentra en el acumulador. Todas las instrucciones aritméticas de SML dejan sus resultados en el acumulador. La instrucción store, +2109, pone (copia) el resultado en la localidad de memoria 09, de donde la toma la instrucción write, +1109, y lo imprime (como número decimal de cuatro dígitos con signo) La instrucción halt, +4300, termina la ejecución. Ejemplo 1 Localidad 00 01 02 03 04 05 06 07 08 09 Número +1007 +1008 +2007 +3008 +2109 +1109 +4300 +0000 +0000 +0000 Instrucción (Lee A) (Lee B) (Carga A) (Suma B) (Almacena C) (Escribe C) (Terminación) (Variable A) (Variable B) (Resultado C) El programa SML del ejemplo 2 lee del teclado dos números y determina e imprime el valor más alto. Note que la instrucción +4107 se utiliza como transferencia de control condicional, a semejanza de la instrucción if de C++. Ejemplo 2 Localidad 00 01 02 03 04 05 06 07 08 09 10 APUNTADORES Y CADENA – LECCIÓN 20 Número +1009 +1010 +2009 +3110 +4107 +1109 +4300 +1110 +4300 +0000 +0000 Instrucción (Lee A) (Lee B) (Carga A) (Resta B) (Bifurca con negativo a 07) (Escribe A) (Terminación) (Escribe B) (Terminación) (Variable A) (Variable B) 20-76 MIGUEL Á. TOLEDO MARTÍNEZ Ahora escriba programas en SML que lleven a cabo las siguientes actividades. a) Mediante un ciclo controlado por centinela lea 10 números positivos y calcule e imprima su suma. b) Mediante un ciclo controlado por contador, lea siete números, algunos positivos y otros negativos, y calcule e imprima su promedio. c) Lea una serie de números y determine e imprima el mayor. El primer número indicará la cantidad de números a procesar. 8. (Simulador de computadora) Al principio parecerá un tanto pretencioso, pero en este problema va a construir su propia computadora. No, no se encargará de soldar sus componentes. En cambio, se valdrá de la poderosa técnica de la simulación basada en software para crear un modelo en software de la Simpletron. No sé decepcionará. Su simulador convertirá en una Simpletron a la computadora en la que esté trabajando y usted de hecho podrá ejecutar, probar y depurar los programas SML que escribió en el problema 18. Cuando ejecute su simulador Simpletron, deberá comenzar por imprimir: *** ¡Bienvenidos a Simpletron! *** *** Por favor introduzca su programa una instrucción *** *** (o dato) a la vez. Presentaré *** *** la localidad y un signo de interrogación (?) . *** *** Después usted tecleará la palabra para esa localidad. *** *** Para detener la introducción de su programa, *** *** teclee el centinela -99999. *** Simule la memoria de la Simpletron con el arreglo memoria de un solo índice, que tiene 100 elementos. Supongamos ahora que está ejecutándose el simulador y examinemos el diálogo a medida que ingresamos el programa del ejemplo 2 del ejercicio 18: 00 ? +1009 01 ? +1010 02 ? +2009 03 ? +3110 04 ? +4107 05 ? +1109 06 ? +4300 07 ? +1110 08 ? +4300 09 ? +0000 10 ? +0000 11 ? -99999 12 *** Se ha completado la carga del programa *** *** Inicia la ejecución del programa *** El programa SML ha sido colocado (o cargado) en el arreglo memoria. Ahora la Simpletron ejecuta su programa SML. La ejecución inicia con la instrucción de la localidad 00 y, como C++, continúa secuencialmente, a menos que se le dirija a otra parte del programa mediante una transferencia de control. Mediante la variable acumulador represente el registro del acumulador. Emplee la variable contador para llevar el registro de la localidad de memoria que contiene la instrucción en ejecución. En la variable codigoOperacion indique la operación actual, es decir, los dos dígitos de la izquierda de la palabra de instrucción. Mediante la variable operando indique la localidad de memoria sobre la que opera la instrucción actual. Así, operando está formado por los dos dígitos más a la derecha de la instrucción que se está ejecutando en el momento. No ejecute las instrucciones directamente en la memoria. En cambio, transfiera la siguiente instrucción a ejecutar de la memoria a una variable llamada registroInstruccion. Luego tome los dos dígitos de la izquierda, poniéndolos en codigoOperacion, y tome los dos de la derecha y colóquelos en operando. Cuando comienza la operación de la Simpletron, los registros especiales se inicializan a 0. APUNTADORES Y CADENA – LECCIÓN 20 20-77 MIGUEL Á. TOLEDO MARTÍNEZ Ahora hagamos el recorrido por la ejecución de la primera instrucción SML, +1009. que está en la localidad de memoria 00. A esto se le llama ciclo de ejecución de instrucción. contador nos dice la localidad de la siguiente instrucción a ejecutar. Obtenemos de la memoria el contenido de dicha localidad mediante la instrucción de C++. registroInstruccion = memoria[contador]; El código de operación y el operando se extraen del registro de instrucciones por medio de las instrucciones: codigoOperacion = registroInstruccion / 100; operando = registroInstruccion %100; Ahora la Simpletron deberá determinar que el código de operación es en realidad una read, o lectura, (en lugar de write, load, etc.) Una switch se encarga de diferenciar las doce operaciones de SML. En la estructura switch se simula el comportamiento de varias instrucciones SML como sigue (dejaremos las demás para que las resuelva el lector): read: load: add: branch: halt: cin >> memoria[ operando]; acumulador = memoria[operando]; acumulador += memoria[operando]; Pronto estudiaremos las instrucciones de bifurcación Esta instrucción imprime el mensaje *** Ha terminado la ejecución de Simpletron *** y luego imprime el nombre y contenido de los registros, así como el contenido completo de la memoria. Tal impresión con frecuencia es conocida como vaciado de computadora. Para ayudarle a programar su función de vaciado, la figura 20.13 muestra un ejemplo de formato de vaciado. Observe que el vaciado tras la ejecución del programa Simpletron mostraría los valores reales de las instrucciones y de los datos al momento de terminación de la ejecución. Prosigamos con la ejecución de la primera instrucción de nuestro programa: +1009 en la localidad 00. Como hemos indicado, la instrucción switch simula esto ejecutando la instrucción C++: cin >> memoria[ operando ]; Antes de la ejecución de cin, se deberá presentar un signo de interrogación (?) en la pantalla, para pedirle al usuario una entrada. La Simpletron espera a que el usuario introduzca un valor y oprima la tecla Retorno. El valor queda en la localidad 09. En este punto se ha completado la simulación de la primera instrucción. Lo que queda es preparar a Simpletron para que ejecute la siguiente instrucción. Dado que la instrucción que se acaba de ejecutar no fue una transferencia de control, simplemente necesitamos incrementar el registro del contador de instrucciones como sigue: ++contador; Con esto se completa la ejecución simulada de la primera instrucción. El proceso completo (es decir, el ciclo de ejecución de instrucción) inicia nuevamente con la obtención de la siguiente instrucción a ejecutar. Ahora consideremos la manera como se simulan las instrucciones de bifurcación (las transferencias de control) Todo lo que necesitamos hacer es ajustar el valor de contador de instrucciones. Por lo tanto, se simula la instrucción de bifurcación incondicional (40) dentro de switch con: contador = operando; La instrucción condicional bifurca si el acumulador es igual a cero se simula con: APUNTADORES Y CADENA – LECCIÓN 20 20-78 MIGUEL Á. TOLEDO MARTÍNEZ if ( acumulador == 0) contador = operando; En este punto, usted deberá implementar el simulador Simpletron y ejecutar los programo SML que escribió en el problema 18. Puede embellecer el SML con características adicionales e incluirlas en su simulador. El simulador deberá buscar varios tipos de errores. Durante la fase de carga del programa, por ejemplo, cada número tecleado por el usuario para memoria deberá estar en el rango de -9999 a +9999. El simulador deberá comprobar mediante un ciclo while que cada número digitado esté en este rango y, si no, solicitar al usuario que lo vuelva a teclear hasta que esté correcto. REGISTROS acumulador contador registroInstrucción codigoOperacion operando +0000 00 +0000 00 00 MEMORIA 0 10 20 30 40 50 60 70 80 90 0 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 1 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 2 3 4 5 6 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 Figura 20.13. Ejemplo de vaciado 7 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 8 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 9 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 +0000 Durante la fase de ejecución, el simulador deberá buscar varios errores serios, como división entre cero, códigos de ejecución inválidos, sobrecargas del acumulador (es decir, operaciones aritméticas que den valores mayores que +9999 o menores que -9999) y otros. Tales errores serios se llaman errores fatales. Al detectar un error fatal, el simulador deberá imprimir un mensaje de error como: *** Intento de dividir entre cero *** *** Terminación anormal de Simpletron *** así como un vaciado completo con el formato que indicamos previamente. Con él se ayudará al usuario a localizar el error en el programa. MÁS PROBLEMAS DE APUNTADORES 9. Modifique el programa de barajado y repartición de naipes del programa BARAJAS.CPP, para que las operaciones de barajado y repartición se lleven a cabo mediante la misma función (barajarRepartir()) La función deberá contener una estructura de ciclo anidada semejante a la función barajar() del programa BARAJAS.CPP. APUNTADORES Y CADENA – LECCIÓN 20 20-79 MIGUEL Á. TOLEDO MARTÍNEZ 10. ¿Qué hace este programa? // Problema21.cpp #include <iostream.h> //Para cout y cin void misterio1(char *, const char *); void main(void) { char cadena1[80], cadena2[80]; cout << “Introduzca dos cadenas de caracteres: ”; cin >> cadena1 >> cadena2; misterio1(cadena1, cadena2); cout << cadena1 << endl; }//Fin de main() void misterio1(char *s1, const char *s2) { while(*s1 != ‘\0’) ++s1; for(; *s1 = *s2; s1++, s2++) ; //Instrucción vacía }//Fin de misterio1() 11. ¿Qué hace este programa? //Problema11.cpp #include <iostream.h> //Para cout y cin int misterio2(const char *); void main(void) { char cadena[80]; cout << “Introduzca una cadena de caracteres: ”; cin >> cadena; cout << misterios2(cadena) << endl; }//Fin de main() int misterio2(const char *s) { for(int x = 0; *s != ‘\0’; s++) ++x; return x; en los siguientes segmentos de programa. Si es posible corregir el error, explique cómo. 12. Encuentre }//Fineldeerror misterio2() 13. a) b) c) d) e) f) g) int * numero; cout << numero << endl; float *realPtr; long *enteroPtr; enteroPtr = realPtr; int *x, y; x = y; APUNTADORES Y CADENA – LECCIÓN 20 20-80 MIGUEL Á. TOLEDO MARTÍNEZ h) char s[] = “éste es un arreglo de caracteres”; i) for(; *s != ‘\0’; s++) cout << *s << ‘ ’ ; j) short *numPtr, resultado; k) void *genericPtr = numPtr; a. result = *genericPtr + 7; l) float x = 19.34; m) float xPtr = &x; n) cout << xPtr << endl; o) char *s; p) cout << s << endl; 14. (Quicksort) En los ejemplos y ejercicios de la lección 19 estudiamos las técnicas de ordenamiento de burbuja, ordenamiento en cubetas y ordenamiento por selección. Ahora presentamos la técnica recursiva de ordenamiento llamada Quicksort. El algoritmo básico para un arreglo de un solo índice es el siguiente: a) Paso de particionamiento: tome el primer elemento del arreglo desordenado y determine su localidad final en el arreglo ordenado, es decir, que todos los valores a la izquierda del elemento sean menores que él y todos los valores a la derecha sean mayores. Ahora tenemos un elemento en su lugar correcto y dos subarreglos desordenados. b) Paso recursivo: Efectúe el paso 1 sobre cada subarreglo desordenado. Cada vez que se ejecuta el paso 1 sobre un subarreglo, se coloca otro elemento en su destino final en el arreglo ordenado y se crean dos subarreglos desordenados. Cuando un subarreglo consiste de un elemento, debe estar ordenado, por lo que dicho elemento está en su ubicación final. El algoritmo básico parece bastante sencillo, pero ¿cómo determinamos la posición final del primer elemento de cada subarreglo? Como ejemplo, considere el siguiente conjunto de valores (el elemento en negritas es el elemento de particionamiento; se colocará en su posición en el arreglo ordenado): 37 c) 2 6 4 89 8 10 12 68 45 Comenzando por el elemento más a la derecha del arreglo, compare cada elemento contra 37 hasta que encuentre uno menor que 37, luego intercambie 37 y dicho elemento. El primer elemento menor que 37 es 12, por lo que 37 y 12 se intercambian. El nuevo arreglo queda así: 12 2 6 4 89 8 10 37 68 45 El elemento 12 está en cursivas, indicando que se acaba de intercambiar con 37. d) Comenzando por la izquierda del arreglo, pero iniciando con el elemento que sigue a 12, compare cada elemento contra 37 hasta que se encuentre uno mayor que 37, luego intercambie 37 y dicho elemento. El primer elemento mayor que 37 es 89, por lo que 37 y 89 se intercambian. El nuevo arreglo queda como: 12 e) 6 4 37 8 10 89 68 45 Comenzando por la derecha, pero iniciando con el elemento anterior a 89, compare los demás elementos con 37 hasta encontrar uno menor que 37, luego intercambie 37 y dicho elemento. El primer elemento menor que 37 es 10, por lo que 37 y 10 se intercambian. El nuevo arreglo queda como: 12 f) 2 2 6 4 10 8 37 89 68 45 Comenzando por la izquierda, pero iniciando con el elemento que sigue a 10, compare los elementos contra 37 hasta encontrar uno mayor que 37, luego intercambie 37 y dicho elemento. No hay más elementos mayores que 37, así que, cuando comparamos 37 contra él mismo, sabemos que está en su destino final en el arreglo ordenado. Una vez aplicada la partición al arreglo anterior, quedan dos arreglos desordenados, El subarreglo con valores menores que 37 contiene 12, 2, 6, 4, 10 y 8. El subarreglo con valores mayores que 37 contiene 89, 68 y 45. El ordenamiento continúa con el particionamiento de ambos arreglos de la misma manera que el arreglo original. APUNTADORES Y CADENA – LECCIÓN 20 20-81 MIGUEL Á. TOLEDO MARTÍNEZ Basándose en el análisis anterior, escriba la función recursiva quicksort() que ordena un arreglo de enteros con un solo índice. La función deberá recibir como argumentos un arreglo de enteros, un índice inicial y un índice final. quicksort() deberá llamar a la función partición() para que lleve a cabo el paso de particionamiento. 14. (Recorrido por un laberinto) El siguiente tramado de # y puntos ( . ) es un arreglo de doble índice que representa un laberinto. # # . # # # # # # # # # # . . # . # . # . # . # # . # # . # . . . # . # # . . . . # # # . # . # # # # # . . . . . # . # # . . . # # # # . # . # # . # . # . . . . . . # # . # . # # # # . # # # # . # . . . . . . # . # # . # # # # # # # # . # # . . . . . . . . . . # # # # # . # # # # # # # En este arreglo de doble índice, los # representan las paredes del laberinto y los puntos los cuadros de las rutas posibles a través del laberinto. Sólo se pueden hacer movimientos a una localidad del arreglo que contenga un punto. Hay un algoritmo sencillo de recorrido del laberinto que garantiza encontrar la salida (suponiendo que la hay) Si no la hay, usted llegará nuevamente al punto de inicio. Ponga su mano derecha sobre la pared que está a su derecha y avance. Nunca quite su mano de la pared. Si el laberinto da la vuelta a la derecha, siga por la pared de la derecha. Siempre y cuando no quite su mano de la pared, tarde o temprano llegará a la salida del laberinto. Podría haber una ruta más corta que la que ha tomado, pero está garantizado que saldrá del laberinto si sigue este algoritmo. Escriba la función recursiva travesia() que lo guiará por el laberinto. La función deberá recibir como argumentos un arreglo de caracteres de 12 por 12 que represente el laberinto, así como el punto de inicio de dicho laberinto. A medida que travesia() intente localizar la salida del laberinto, deberá poner una x en cada cuadro de la ruta. La función deberá desplegar el laberinto después de cada movida, de modo que el usuario pueda ver cómo se resuelve el problema. 15. (Generación aleatoria de laberintos) Escriba la función generadorLaberinto(), que toma como argumento un arreglo de caracteres de 12 por 12 y genera aleatoriamente un laberinto. Dicha función deberá proporcionar el punto de inicio y el de terminación del laberinto. Pruebe la función travesía() del problema 25 sobre varios laberintos generados al azar. 16. (Laberintos de cualquier tamaño) Generalice las funciones travesia() y generadorLaberinto() de los problemas 14 y 15 para que procesen laberintos de cualquier anchura y altura. 17. (Arreglos de apuntadores a funciones) Rescriba el programa FUNARRE3.CPP, (de la lección 19, página 50) para que opere mediante una interfaz operada por menú. El programa deberá ofrecer las siguientes 5 opciones al usuario, como sigue (deben aparecer en la pantalla): Introduzca una opción: 0. Imprimir un arreglo de calificaciones 1. Encontrar la mínima calificación 2. Encontrar la máxima calificación 3. Imprimir el promedio de cada estudiante 4. Finalizar el programa Una restricción al uso de arreglos de apuntadores a funciones es que todos los apuntadores deben ser del mismo tipo. Los apuntadores deben ser a funciones del mismo tipo de devolución y que reciban argumentos APUNTADORES Y CADENA – LECCIÓN 20 20-82 MIGUEL Á. TOLEDO MARTÍNEZ del mismo tipo. Por esta razón, las funciones del programa FUNARRE3.CPP se deben modificar para que todas devuelvan el mismo tipo y tomen los mismos parámetros. Modifique las funciones minimo() y máximo() de modo que impriman el valor mínimo o máximo y no devuelvan nada. En la opción 3, modifique la función promedio() del programa FUNARRE3.CPP para que envíe a la salida el promedio de cada estudiante (no de uno específico) La función promedio() no deberá devolver nada y deberá tomar los mismos parámetros que mostrarArreglo(), minimo() y máximo(). Almacene los apuntadores a las cuatro funciones en el arreglo procesarOpcion() y tome la selección del usuario como el índice del arreglo para llamar a cada función. 18. (Modificaciones al simulador Simpletron) En el problema 7 se escribió un simulador en software de una computadora que ejecuta programas escritos en el SML (lenguaje de máquina Simpletron) En este problema proponemos vanas modificaciones y mejoras al simulador Simpletron. Algunas de las siguientes modificaciones y mejoras podrían ser necesarias para ejecutar los programas generados por el compilador. a) Aumente la memoria del simulador Simpletron de modo que contenga 1000 localidades de memoria, permitiendo el manejo de programas más extensos. b) Haga que el simulador pueda efectuar cálculos de módulo. Esto requiere una nueva instrucción en el lenguaje de máquina Simpletron. c) Permita que el simulador realice cálculos de exponenciación. Esto requiere una nueva instrucción en el lenguaje de máquina Simpletron. d) Modifique el simulador para que emplee valores hexadecimales, en lugar de valores enteros, para representar instrucciones en lenguaje de máquina Simpletron. e) Modifique el simulador de modo que permita enviar a la salida un salto de línea. Esto requiere una nueva instrucción en el lenguaje de máquina Simpletron. f) Modifique el simulador para que procese valores de punto flotante, además de valores enteros. g) Modifique el simulador a fin de que maneje entrada de cadenas. Sugerencia: es posible dividir cada palabra Simpletron en dos grupos, conteniendo cada una un entero de dos dígitos. Cada entero de dos dígitos representa el equivalente decimal ASCII de un carácter. Agregue una instrucción en lenguaje de máquina que acepte la entrada de una cadena y la almacene a partir de cierta localidad de memoria de la Simpletron. La primera mitad de la palabra en dicha localidad será la cuenta del número de caracteres de la cadena (es decir, la longitud de la cadena) Cada media palabra subsiguiente contiene un carácter ASCII, expresado como dos dígitos decimales. La instrucción en lenguaje de máquina convierte cada carácter en su equivalente ASCII y lo asigna a media palabra. h) Modifique el simulador para que maneje salida de cadenas almacenadas en el formato de la parte (g) Sugerencia: agregue una instrucción de lenguaje de máquina que imprima una cadena a partir de cierta localidad de memoria de la Simpletron. La primera mitad de la palabra de dicha localidad es la cuenta del número de caracteres de la cadena (es decir, la longitud de la cadena) Cada media palabra subsiguiente contiene un carácter ASCII expresado como dos dígitos decimales. La instrucción de lenguaje de máquina comprueba la longitud e imprime la cadena, traduciendo cada número de dos dígitos a su carácter equivalente. 19. ¿Qué hace este programa? //Programa19.cpp #include <iostream.h> //Para cout y cin int misterio3(const char *, const char *); void main(void) { char cadena1[80], cadena2[80]; cout << “Introduzca dos cadenas de caracteres: “; cin >> cadena1 >> cadena2; cout <<“El resultado es “ << misterio3(cadena1, cadena2) << endl; }//Fin de main() int misterio3(const char *s1, const char *s2) APUNTADORES Y CADENA – LECCIÓN 20 20-83 MIGUEL Á. TOLEDO MARTÍNEZ { for(; *s1 != ‘\0’ && *s2 != ‘\0’; s1++, s2++) if(*s1 != *s2) return 0; return 1; }//Fin de misterio3() PROBLEMAS DE MANIPULACIÓN DE CADENAS 20. Escriba un programa que, mediante la función strcmp, compare dos cadenas introducidas por el usuario. El programa deberá indicar si la primera cadena es menor, igual o mayor que la segunda. 21. Escriba un programa que compare, mediante la función strncmp, dos cadenas introducidas por el usuario. El programa deberá aceptar como entrada el número de caracteres a comparar, además de indicar si la primera cadena es menor, igual o mayor que la segunda. 22. Escriba un programa que se valga de la generación de números aleatorios para crear oraciones. El programa utilizará cuatro arreglos de apuntadores a char, llamados articulo, sustantivo, verbo y preposicion. Deberá crear una oración seleccionando una palabra al azar de cada arreglo en el siguiente orden articulo, sustantivo, verbo, preposicion, articulo y sustantivo. A medida que se selecciona cada palabra, se le concatenará con las palabras previas en un arreglo lo bastante grande para guardar la oración completa. Las palabras se separarán por espacios. La impresión de la oración generada comenzará con una letra mayúscula y terminará con un punto. El programa generará 20 oraciones. Los arreglos se llenarán como sigue: el arreglo articulo deberá contener los artículos “e”, “un”, “y”, “algún” y, “cualquier”; el arreglo sustantivo deberá contener los sustantivos “niño”, “señor”, “perro”, “pueblo” y “auto”; el arreglo verbo deberá contener los verbos “manejo”, “saltó”, “corrió”, “caminó” y “evitó”; el arreglo preposicion deberá contener las preposiciones “a”, “de”, “sobre”, “bajo” y “en”. Una vez que el programa haya sido escrito y esté operando, modifíquelo para que genere una historia corta que consista de varias oraciones. (¿Qué tal la posibilidad de un escritor de cuentos para la clase de literatura?) 23. (Quintillas) Una quintilla es una oración en verso, a veces jocoso, de cinco líneas, en el que el primero y segundo versos riman con el quinto y el tercero con el cuarto. Mediante técnicas parecidas a las desarrolladas en el problema 22, escriba un programa en C++ que produzca quintillas al azar. Refinar este programa para que genere buenas quintillas es un problema complejo, pero el resultado vale el esfuerzo. 24. Escriba un programa que codifique frases en pig Latin. (En inglés, pig Latin es un tipo de lenguaje que se utiliza con propósitos de juego.) Hay muchas variaciones en los métodos con los que se forman frases en pig Latin. Por simplicidad, utilice el siguiente algoritmo: Para formar una frase en pin Latin, divida en palabras dicha frase mediante la función strtok. Para convertir cada palabra a pig Latin, ponga la primera letra de la palabra al final de ella y agregue las letras “ay”. Así, la palabra “salta” se convierte en “altasay”, “las” se vuelve “aslay” y “computadora” queda corno “omputadoracay”. Los espacios entre las palabras quedan igual. Suponga lo siguiente: la frase consiste en palabras separadas por espacios, no hay signos de puntuación y todas las palabras tienen dos o más letras. La función mostrarLatinPalabra() deberá desplegar cada palabra. Sugerencia: cada vez que se encuentre un token durante una llamada a strtok, pase el apuntador del token a la función mostrarLatinPalabra() e imprima la palabra en pig Latin. 25. Escriba un programa que acepte como entrada una cadena con un número telefónico, en la forma (555)5555555. El programa deberá utilizar la función strtok para obtener como token el código de área, los primeros tres dígitos del número telefónico y los últimos cuatro dígitos. Los siete dígitos del número telefónico deben concatenarse para formar una sola cadena. El programa deberá convertir la cadena con el código de área a int y la cadena con el número telefónico a long. Se deben imprimir el código de área y el número telefónico. 26. Escriba un programa que acepte como entrada una línea de texto, la divida en tokens por medio de la función strtok y envíe los tokens a la salida en orden inverso. 27. Utilice las funciones de comparación de cadenas estudiadas y las técnicas de ordenamiento de arreglos desarrollados para escribir un programa que alfabetice una lista de cadenas. Ordene los nombres de 10 o 15 pueblos de su zona. APUNTADORES Y CADENA – LECCIÓN 20 20-84 MIGUEL Á. TOLEDO MARTÍNEZ 28. Escriba dos versiones de la función de copia de cadenas y de la función de concatenación de cadenas de la figura 20.9. La primera versión deberá utilizar indización de arreglos, y la segunda apuntadores y aritmética de apuntadores. 29. Escriba dos versiones de cada función de comparación de cadenas de la figura 20.9. La primera versión debe emplear indización de arreglos, y la segunda apuntadores y aritmética de apuntadores. 30. Escriba dos versiones de la función strlen de la figura 20.9. La primera versión debe valerse de indización de arreglos, y la segunda de apuntadores y aritmética de apuntadores. SECCIÓN ESPECIAL: Manipulación avanzado de cadenas Los problemas anteriores están ligados a las lecciones y se diseñaron para poner a prueba los conocimientos del lector sobre los conceptos fundamentales de manipulación de cadenas. Esta sección incluye un conjunto de problemas de manipulación de cadenas intermedios y avanzados. El lector deberá encontrar interesantes y amenos estos problemas. La dificultad de los problemas varía considerablemente. Algunos requieren de una hora o dos para su escritura e implementación. Otros son útiles corno tareas de laboratorio que podrían tardar dos o tres semanas de estudio e implementación. Varios son proyectos vastos que se extenderán durante todo el curso. 31. (Análisis de texto) La disponibilidad de computadoras con capacidad de manipulación de cadenas ha dado como resultado algunos enfoques bastante interesantes para el análisis de las obras de los grandes autores. Se ha puesto mucha atención en si William Shakespeare en realidad existió. Algunos estudiosos creen que hay bastante evidencia que indica que las obras maestras atribuidas a Shakespeare en realidad fueron escritas por Christopher Marlowe u otros autores. Los investigadores se han valido de las computadoras para encontrar las similitudes en los escritos de ambos autores. Este ejercicio examina tres métodos de análisis de texto mediante computadoras. a) Escriba un programa que lea del teclado varias líneas de texto e imprima una tabla que indique el número de veces que sucede cada letra del abecedario en el texto. Por ejemplo, la frase: Ser o no ser: ésa es la cuestión contiene dos “a”, ninguna “b”, una “c”, etcétera. b) Escriba un programa que lea varias líneas de texto e imprima una tabla que indique el número de palabras de una letra, de dos letras, de tres letras, etc., que hay en el texto. Por ejemplo, la frase Si es más noble para la mente sufrir Contiene c) Longitud de la palabra Veces 1 2 3 4 5 6 7 0 3 1 1 2 1 0 Escriba un programa que lea varias líneas de texto e imprima una tabla que indique el número veces que sucede cada palabra del texto. La primera versión del programa deberá incluir las palabras de la tabla en el mismo orden en que aparecen en el texto. Por ejemplo, las líneas: Ser o no ser: ésa es la cuestión Si es más noble para la mente sufrir contienen la palabra “ser“ dos veces, la palabra “más” una vez, “no” una vez, etc. Después deberá intentar una impresión más interesante (y útil) en la que las palabras aparezcan ordenadas alfabéticamente. 32. (Procesamiento de texto) Una importante función de los sistemas de procesamiento de texto es la justificación tipográfica, es decir, la alineación de las palabras tanto con el margen izquierdo como con el derecho. Esto APUNTADORES Y CADENA – LECCIÓN 20 20-85 MIGUEL Á. TOLEDO MARTÍNEZ genera un documento de aspecto profesional que da la apariencia de haber sido formado tipográficamente, en lugar de escrito en una máquina de escribir. La justificación tipográfica puede lograrse en los sistemas de cómputo insertando caracteres en blanco entre las palabras, de modo que la palabra más a la derecha quede alineada con el margen derecho. Escriba un programa que lea varias líneas de texto y lo imprima en formato justificado. Suponga que texto se debe imprimir en papel de 8 ½ pulgadas de ancho y que se dejan márgenes de una pulgada tanto al lado izquierdo como al derecho. Suponga que la computadora imprime 10 caracteres por pulgada horizontal. Por lo tanto, dicho programa deberá imprimir 6 ½ pulgadas de texto o 65 caracteres por línea. 33. (Impresión de fechas con varios formatos) En la correspondencia comercial, las fechas por lo general se imprimen en varios formatos. Dos de los más comunes son: 21/07/55 y julio 21, 1955 Escriba un programa que lea una fecha con el primer formato y la imprima con el segundo. 34. (Protección de cheques) Con frecuencia las computadoras se utilizan para ejecutar sistemas de impresión de cheques, como sucede con las aplicaciones de nómina y cuentas por pagar. Circulan por allí muchas historias extrañas sobre cheques de nómina emitidos (accidentalmente) que amparan cifras de más de un millón. Los sistemas de cheques imprimen cifras extrañas debido a errores humanos y/o a fallas de la máquina. Los diseñadores de sistemas integran controles en sus sistemas para evitar la emisión de tales cheques. Otro problema serio es que alguien altere intencionalmente la cifra de un cheque para cobrarlo fraudulentamente. Para evitar la alteración de una cifra, la mayoría de los sistemas de impresión de cheques utiliza una técnica llamada protección de cheques. Los cheques diseñados para impresión en una computadora contienen una cantidad fija de espacios en los que se puede imprimir una cifra. Suponga que un cheque contiene ocho espacios en blanco, donde la computadora debe imprimir el importe de un cheque de nómina. Si la cifra es grande, entonces se llenarán los ocho espacios. Por ejemplo: 1,230.60 ----------12345678 (importe del cheque) (posiciones) Por otra parte, si la cifra es menor que $100.00, entonces quedarían en blanco varios de los espacios. Por ejemplo, 99.87 -----------12345678 contiene tres espacios en blanco. Si un cheque se imprime con espacios en blanco, es fácil de alterar. Para evitar esto, muchos sistemas de impresión de cheques protegen la cifra insertando asteriscos iniciales como sigue: ***99.87 ----------12345678 Escriba un programa que acepte una cifra y luego la imprima en formato de protección de cheques, con asteriscos iniciales, de ser necesario. Suponga que se dispone de nueve espacios para imprimir la cifra. 35. (Escritura del importe de un cheque con palabras) Prosiguiendo el estudio del ejemplo previo, reiteramos la importancia de diseñar sistemas de impresión de cheques que eviten la alteración de sus importes. Un método de seguridad común requiere que la cifra sea escrita tanto con números como con palabras. Inclusive si alguien es capaz de alterar la cifra numérica, es muy difícil modificar la cifra en palabras. APUNTADORES Y CADENA – LECCIÓN 20 20-86 MIGUEL Á. TOLEDO MARTÍNEZ Muchos sistemas computarizados de impresión de cheques no imprimen la cifra del cheque con palabras. Tal vez la razón principal de esta omisión es que la mayoría de los lenguajes de alto nivel empleados para las aplicaciones comerciales no cuentan con características adecuadas de manipulación de cadenas. Otra razón es que la lógica de la escritura de las cifras con palabras es un tanto complicada. Escriba un programa en C++ que acepte una cifra numérica y la imprima con palabras. Por ejemplo, la cifra 112.43 deberá escribirse como: CIENTO DOCE con 43/100 36. (Código Morse) Tal vez el esquema de codificación más famoso de todos es el código Morse, desarrollador por Samuel Morse en 1832 para el sistema telegráfico. El código Morse asigna una serie de puntos y rayas a cada letra del abecedario, a cada dígito y a algunos caracteres especiales (punto, coma, dos puntos y punto y coma) En los sistemas audibles, el punto representa un sonido corto y la raya un sonido largo., Otras representaciones de puntos y rayas son las empleadas en los sistemas de orientación por luz y los de señalización con indicadores. La separación entre palabras se indica mediante un espacio o, sencillamente, por la ausencia de un punto o raya. En los sistemas sonoros, se indica un espacio mediante un periodo corto en el que no se transmite ningún sonido. En la figura 20.14 aparece la versión internacional del código Morse. Escriba un programa que lea una frase en español y la codifique en código Morse. También escriba un programa que lea una frase en código Morse y la convierta en su equivalente en inglés. Ponga un espacio entre cada letra codificada en Morse y tres espacios entre cada palabra. Carácter Código Carácter Código A .T B -... U ..C -.-. V ...D -.. W .-E . X -..F ..-. Y -.-G --. Z --.. H .... I .. Dígitos J .--1 .---K -.2 ..--L .-.. 3 ...-M -4 ....N -. 5 ..... O --6 -.... P .--. 7 --... Q --.8 ---.. R .-. 9 ----. S ... 0 ----Figura 20.14. Las letras del abecedario como se expresan con el código Morse internacional 37. (Programa de conversión entre Sistema métrico y sistema inglés) Escriba un programa que apoye al usuario en las conversiones entre el sistema métrico y el inglés. El programa deberá permitirle al usuario especificar como cadenas los nombres de las unidades (es decir, centímetros, litros, gramos, etc., en el sistema métrico y pulgadas, cuartillos, libras, etc., en el sistema inglés) y deberá responder a preguntas sencillas como: “¿Cuántas pulgadas hay en 2 metros?” “¿Cuántos litros hay en 10 cuartillos?” El programa deberá reconocer las conversiones inválidas. Por ejemplo, la pregunta: “¿Cuántos pies hay en 5 kilogramos” APUNTADORES Y CADENA – LECCIÓN 20 20-87 MIGUEL Á. TOLEDO MARTÍNEZ no tiene sentido, pues los pies, son unidades de longitud, mientras que los kilogramos, son unidades de peso. INTERESANTE PROYECTO DE MANIPULACIÓN DE CADENAS 38. (Generador de crucigramas) Mucha gente ha resuelto crucigramas, pero pocos han intentado generar uno. La generación de crucigramas es difícil. Aquí se sugiere como un proyecto de manipulación de cadenas de alta complejidad y esfuerzo. Hay muchos asuntos que el programador debe resolver para lograr que funcione incluso el programa generador de crucigramas más sencillo. Por ejemplo, ¿cómo se representa el tramado de un crucigrama en la computadora? ¿Deberá utilizarse una serie de cadenas o arreglos de doble índice? El programador necesita una fuente de palabras (es decir, un diccionario computarizado) al que pueda hacer referencia directa el programa. ¿De qué forma deben almacenarse estas palabras para simplificar las complejas manipulaciones requeridas por el programa? El lector realmente ambicioso deseará generar la parte de claves del crucigrama, donde se imprimen las pistas para determinar las palabras horizontales y verticales. La simple impresión de una versión en blanco del crucigrama es un problema complejo. APUNTADORES Y CADENA – LECCIÓN 20 20-88