Capítulo 9. Punteros y asignación dinámica de memoria. 1. Definición de puntero. 2. Operadores que se aplican a punteros. 3. Expresiones con punteros. 4. Punteros y arrays. 4.1. Arrays de punteros. 5. Punteros a punteros: Indirección múltiple. 6. Punteros a funciones. 7. Asignación dinámica de memoria. 8. Problemas con punteros. 1 1. Definición de Puntero. Un puntero es una variable cuyo contenido es una dirección de memoria. Cuando esta dirección es la posición de memoria de otra variable, se dice que el puntero apunta a esta variable. Por tanto, podemos hacer la siguiente diferenciación: - Dirección de memoria que ocupa el puntero: un puntero es una variable de memoria, por tanto ocupa una dirección de la misma. - Dirección de memoria a la que apunta el puntero: el contenido de un puntero es una dirección de memoria, a la cual se dice que apunta el puntero. Si la dirección a la que apunta es la de otra variable, se dice que el puntero apunta a esa variable. - Contenido de la variable a la que apunta el puntero: la variable que está siendo apuntada tendrá un contenido, que podríamos llamar el dato. De forma gráfica, un ejemplo podría representarse de la siguiente manera: Etiq. Direc. P 1000 ... ... V 5000 ... Memoria Contenido 5000 ... ... 13,55 ... En ese caso, P es una variable de tipo puntero, ya que su contenido (5000) es una dirección de memoria. El puntero P ocupa la dirección de memoria 1000, pero su contenido es la dirección 5000. Esta dirección es la que ocupa la variable V, por lo que se dice que P apunta a V. El contenido de V es realmente el dato en este caso, cuyo valor es 13,55. Se suele decir que un puntero es un caso de indirección a una variable. Para todo lo relacionado con punteros usaremos la sintaxis de C también para pseudocódigo. Un puntero, como una variable que es, debe ser declarada como cualquier otra variable. En C un puntero se declara como sigue: <tipo> *<nombre_puntero>; donde tipo es el tipo de la variable a la que puede apuntar el puntero. Ej. int *pi;//pi podrá apuntar a variables de tipo int. char *pc;//pc podrá apuntar a variables de tipo char. float *pf;//pf podrá apuntar a variables float. 2 Debe recordarse que puede visualizarse el contenido de un puntero, es decir a qué dirección apunta, en formato hexadecimal, con el especificador %p en la función printf: Ej. int x; int *pi = &x; ... printf(“Dirección que ocupa x: %p”, pi); printf(“Dirección que ocupa x: %p”, &x); //Ambos printf visualizan lo mismo. Debe recordarse que cada posición de memoria almacena 1 byte, es decir que el tipo de cada variable indicará cuántas posiciones de memoria ocupa la variable, así tenemos: char, 1 byte; int, 2 bytes; float, 4 bytes, etc. 2. Operadores que se aplican a punteros. Existen dos operadores en C especialmente relacionados con los punteros. Son los siguientes: - Operador &: se aplica a una variable y devuelve la dirección de memoria que ocupa esa variable. Por tanto el valor que nos devuelve podrá almacenarse en un puntero. Ej. int *pi; //pi es un puntero a variables de tipo int. int x; //x es una variable de tipo int. pi = &x; //Con esta asignación hacemos que el puntero //pi apunte a la variable x, ya que &x nos //devuelve la dirección de x. - Operador *: se aplica a un puntero y devuelve el contenido de la variable a la que apunta el puntero. Ej. int *pi; int x = 1; int y; pi = &x; //pi apunta a x. Después de esta asignación //x será equivalente a *pi. Por tanto: y = *pi; *pi = 0; //Almacena en y el contenido de x, ya que //*pi es equivalente a x. //Almacena 0 en x. 3. Expresiones con Punteros. 3 Los punteros son un tipo especial de variable, por lo que no todas las operaciones de C se pueden realizar sobre ellos. Veamos las que sí se pueden hacer: - Inicialización de punteros: un puntero, una vez declarado, contiene un valor desconocido y por ese motivo debe ser inicializado. Si se intenta usar un puntero sin inicializar podemos incluso dañar el sistema operativo, ya que puede estar apuntando a cualquier dirección de memoria. La forma de inicializar un puntero es asignarle una dirección conocida, es decir de una variable declarada. Ej. int *pi; int x; pi = &x; //Apunta a x, dirección conocida. Para indicar que un puntero inicialmente no apunta a nada se le puede asignar la constante NULL (en mayúsculas), que está declarada en stdio.h y en stdlib.h. Esta asignación no implica que el puntero ya apunta a una dirección conocida. El valor NULL es simplemente como bandera que puede usar el programador para indicar en el programa que el puntero apunta a una dirección no concretada. Por tanto, si se usa un puntero que apunta a NULL también puede dañarse el sistema operativo. Ej. int *pi; pi = NULL; - //pi apunta a una dirección desconocida. Asignación de punteros: un puntero puede ser asignado a otro siempre que ambos sean punteros del mismo tipo de variables. Después de la asignación, ambos punteros apuntarán a la misma variable. Ej. int *ptr1, *ptr2; int numero; ptr1 = &numero; ptr2 = ptr1; //ptr2 apuntará a donde ptr1, es decir a //la variable numero. - Aritmética de punteros: un puntero tan sólo puede sumarse o restarse con números enteros. Ninguna otra operación está permitida con punteros. Cada unidad que se le sume a un puntero hará que éste se incremente en el número de bytes (nº de posiciones de memoria) que ocupa el tipo al que apunta. Con la resta el puntero se decrementa de la misma forma. Por ejemplo, si p es un puntero al tipo int, p+1 apuntará a la dirección de memoria que está dos posiciones posteriores a la que apunta p, ya que el tipo int ocupa 2 bytes. Si se usa p+5, el puntero p se incrementa en 10 posiciones de memoria, ya que cada incremento supone 2 posiciones (5*2=10). 4 Ej. int n, *p = &n; //n es int; p es puntero a n. p++; //Suma 1 a p, por tanto p se incrementa en dos //posiciones de memoria (int ocupa 2 bytes). char c, *p = &c; //c es char; p es puntero a c. p--; //Resta 1 a p, por tanto p se decrementa en una //posición de memoria (char ocupa 1 byte). float f, *p = &f; //c es float; p es puntero a f. p += 3; //Suma 3 a p, por tanto p se incrementa en 12 //posiciones de memoria: 3*4 (float = 4 bytes) Estas operaciones se usan cuando se tienen varias variables del mismo tipo en direcciones consecutivas de memoria, como ocurre con los arrays. Si un puntero apunta a la primera componente del array, al sumarle una unidad apuntará a la segunda componente, ya que se incrementa en el número de posiciones que ocupe cada componente. Si se le suma 5 al puntero, apuntará a la quinta componente del array. Igualmente ocurre con la resta, pero en el sentido inverso. En general, sumar o restar enteros a punteros se usa para recorrer arrays, como veremos en un apartado posterior. Debe recordarse que declarar varias variables seguidas en un programa no implica que ocupen posiciones consecutivas en memoria. Por este motivo, para variables que no sean arrays no suele usarse esta aritmética de punteros. Se pueden aplicar también los operadores ++ punteros, como se ve en algún ejemplo anterior. - -- += -= a Comparación de punteros: los punteros pueden ser comparados en una expresión relacional como cualquier variable. Si un puntero es menor que otro, apunta a una dirección de memoria más baja, es decir previa; si es mayor, apunta a una dirección más alta o posterior. Ej. int n, m; int *p = &n, *q = &m; if (p > q) ... //si p apunta a una dirección posterior 4. Punteros y Arrays. En C existe una relación muy estrecha entre arrays y punteros, debido a que el nombre de un array para C es un puntero a la primera componente del array. Cuando se usa un array en un programa, se suele indicar mediante corchetes a qué componente se quiere acceder. Pero si se usa el nombre del array sólo, sin indicar ninguna componente entre corchetes, será un puntero a la primera componente. 5 Ej. int notas[10], *p; ... notas[0] = 5; //En la componente 0 almacena un 5, //se ha indicado componente entre []. p = notas+1; //p apunta a la componente 1. El nombre //del array, notas, es un puntero a la //componente 0, al sumar 1 pasa a la 1. Por tanto, suponiendo el array notas anterior, las dos expresiones siguientes tienen el mismo valor: notas El nombre del array es un puntero a la primera componente del array. &notas[0] También es un puntero a la primera componente, ya que & nos da su dirección. Por tanto, usando el nombre del array y la aritmética de punteros se puede acceder a cada componente del array. Ej. int dato[3] = {1,2,3}; printf("%d", *dato); printf("%d", *(dato+2) ); //visualiza el 1. //visualiza el 3. Una acción que no debe realizarse es modificar la dirección donde apunta el nombre del array. El nombre del array debe mantenerse apuntando siempre a la primera componente, que es el valor que toma por defecto. Si se desea tener un puntero que apunte a otra componente que no sea la primera, se suele declarar otro puntero del tipo del array. Ej. int int ptr ptr ptr dato[3] = {1,2,3}; *ptr; = dato; //ptr apunta al 1. = &dato[0]; //ptr apunta al 1. = &dato[2]; //ptr apunta al 3. //la dirección dato no se ha modificado. Ej. char cadena[80]; char *ptr; int i; scanf(“%79[^\n]”, cadena); fflush(stdin); ptr = cadena; for (i=0; i<strlen(cadena); i++) //Visualiza cada printf("%c", *ptr++ ); //letra de la cadena. for (i=0; i<strlen(cadena); i++) //Hace lo mismo que printf("%c", cadena[i] ); //el for anterior. 6 Ambos bucles for son equivalentes: el primero for usa aritmética de punteros; el segundo usa índices de array. En el caso de cadenas (arrays de caracteres) se pueden declarar usando punteros, recogiendo la longitud de la inicialización, sin poner corchetes: Ej. char *cadena = "ESTO ES UNA CADENA"; //cadena apunta a la primera E. //cadena+3 apunta a la O. Etc.. 4.1. Arrays de Punteros. Como cualquier otro tipo de datos, los punteros pueden almacenarse en arrays, como un conjunto de punteros del mismo tipo. La sintaxis de la declaración de un array de punteros es: <tipo> *<nombre_puntero>[tamaño]; donde tipo será el tipo de datos a los que pueden apuntar los punteros, y tamaño será el número de punteros que contiene el array. Ej. int numero[4]; //array de 4 números int. int *ptr[4]; //array de 4 punteros a int. ptr[0] = &numero[0]; ptr[1] = &numero[1]; //Cada puntero apunta a un número ptr[2] = &numero[2]; //del array. ptr[3] = &numero[3]; //Después de esas asignaciones sería lo mismo usar //*ptr[i] que numero[i]. printf("%d", *ptr[2] ); //visualiza el contenido del //puntero ptr[2], numero[2] Un uso común de los arrays de punteros es en la gestión de mensajes, donde cada puntero del array apunta a una cadena de caracteres (mensaje). Ej. Array de 3 punteros a cadenas. char *error[] = { "Acceso denegado\n", "Fallo en la comunicación\n", "Dato erróneo\n"}; if ( codigo < 0 ) printf("%s",error[2]); //Visualiza “Dato erróneo”. Si en este ejemplo anterior el array se decleara como array bidimensional, las 3 cadenas tendrán la misma longitud, que será la mayor de las 3, por lo que de esta forma se ocuparía más memoria. 7 Ej. El ejemplo anterior declarando un array bidimensional de 3 cadenas. char error[][26] = { "Acceso denegado\n", "Fallo en la comunicación\n", "Dato erróneo\n"}; //Será un array de 3 filas y 26 columnas. La longitud //de la mayor cadena es 26. if ( codigo < 0 ) //La sintaxis al visualizar printf("%s",error[2]); //es igual que con punteros. Un array bidimensional puede ser considerado como un array de punteros, ya que cada fila del array bidimensional es un array de una dimensión. Como todo nombre de array es un puntero, cada fila será un puntero. Dicho de otra forma, con el array bidimensional int dato[3][4] puede decirse que datos es una array de 3 punteros: dato[0], dato[1], dato[2]. Cada uno de esos punteros apunta a un array de 4 componentes, concretamente al primer elemento de cada fila del array bidimensional. Para acceder a esas componentes puede usarse aritmética de punteros en lugar de índices. Ej. int dato[3][4]; //array de 3 filas y 4 columnas. Cada fila es una array de 4 elementos, es un puntero: dato[0] es el nombre de un array de 4 elementos, por tanto es un puntero al primer elemento. Con dato[1] y dato[2] ocurre lo mismo que con dato[0]. Tenemos un array de 3 punteros. Se producen así las siguientes equivalencias: Aritmética *dato[0] *(dato[0]+1) *(dato[2]+3) dato[1]+2 <=> <=> <=> <=> Indices dato[0][0] dato[0][1] dato[2][3] &dato[1][2] (es un puntero) Podrían representarse como 3 punteros: dato[0] dato[1] dato[2] Como vemos en ese ejemplo el segundo índice, la columna, puede ser evitado (no se usa corchetes) y en su lugar emplea aritmética de punteros. Para indicar la fila sí se usa índice. Veamos otro ejemplo en el que se usa otro puntero para apuntar al array bidimensional: Ej. int dato[3][4]; //array de 3 filas y 4 columnas. int *p; p = dato[1]; //p apunta al elemento dato[1][0]. 8 printf(“%d”, *(p+2)); //Visualiza dato[1][2]. p++; //p apunta al elemento dato[1][1]. printf(“%d”, *p); //Visualiza dato[1][1]. Si se tiene en cuenta que un array bidimensional está almacenado en memoria por filas, es decir primero los elementos de la fila 0, después los de la fila 1 y así sucesivamente, en el ejemplo anterior podría considerarse como 12 componentes seguidas, por lo que el puntero p puede avanzar de 0 a 11. Este tipo de acceso no suele usarse. Veamos un ejemplo: Ej. int dato[3][4]; int *p, i; p = dato[0]; //p apunta al elemento dato[0][0]. //Equivale a p=&dato[0][0] for (i = 0; i < 12; i++) //Bucle de 0 a 11. printf(“\n %d”, *(p+i)); //Visualiza cada elemento. En los ejemplos vistos el array dato es un array de 3 punteros. Si es un array quiere decir que es un puntero al primer elemento del array, que a su vez es otro puntero. Por tanto dato es un puntero a otro puntero, un caso de indirección múltiple como vamos a ver en el siguiente apartado. 5. Punteros a Punteros: Indirección Múltiple. Normalmente un puntero apunta a una variable que contiene un dato. Pero puede darse la indirección múltiple, es decir que un puntero apunte a otro puntero. En este caso un puntero apunta a una variable que contiene una dirección, por lo que es otro puntero, que apunta a otra variable que es la que contiene el dato. Puntero Variable Dirección Dato Indirección simple Puntero Dirección Puntero Dirección Indirección múltiple Variable Dato La forma de declarar la indirección múltiple es con dos asteriscos: <tipo_de_datos> Ej. float **num; **<puntero>; //num es puntero a otro puntero, éste //segundo puntero apunta a dato float. Para acceder al dato final, indirectamente apuntado, se usa * dos veces: 9 Ej. int dato, *puntero, **indirecto; dato = 5; puntero = &dato; indirecto = &puntero; //La dir. de puntero se guarda //en indirecto. printf ("%d", **indirecto); //Visualiza dato: 5. Como decíamos en el apartado anterior, el nombre de un array bidimensional puede ser tratado como un caso de indirección múltiple. El array int dato[3][4] es un array de 3 punteros, dato[0], dato[1], dato[2]. Como todo array es un puntero, dato será un puntero que apunta al primero de los 3 punteros (dato[0]). Esto se puede representar como sigue: dato dato[0] dato[1] dato[2] Es decir, dato es un puntero a otro puntero. Por tanto puede aplicarse la indirección múltiple para acceder a los elementos del array, sin usar ni un sólo índice. Esto quiere decir que se cumplen las siguientes equivalencias: Indirección Múltiple *(*dato) *(*(dato+1)) *(*(dato+1)+2) *(dato+1) *(dato+2)+1 <=> <=> <=> <=> <=> Array de punteros *dato[0] *dato[1] *(dato[1]+2) dato[1] dato[2]+1 <=> <=> <=> <=> <=> Indices dato[0][0] dato[1][0] dato[1][2] &dato[1][0] &dato[2][1] Para recorrer el array dato completo usando aritmética de punteros (sin índices) sería: Ej. Recorrer array bidimensional con aritmética de punteros: int dato[3][4]; int i, j; for (i = 0; i < 3; for (j = 0; j < printf(“%d”, //Al ser i++) 4; j++) *(*(dato + i) + j) ); array bidim., se usan 2 asteriscos. Si en lugar de utilizar el nombre del array, se quiere emplear otro puntero para acceder al array bidimensional, este puntero se declarará como se indica en el siguiente ejemplo. Ej. int dato[3][4]; int *p; 10 p = *dato; //*dato equivale a dato[0]. Por tanto p apunta al //primer elemento de la fila 0, y p+3 apunta al último //elemento de la fila 0. printf(“%d”, *(p + 3)); //Visualiza dato[0][3]. printf(“%d”, *(*dato + 3)); //Visualiza dato[0][3]. Teniendo en cuenta que un array bidimensional se almacena en memoria de forma contigua por filas, con el puntero p del ejemplo anterior puede recorrerse el array dato como si fuera unidimensional de 12 elementos (3 filas x 4 columnas = 12 elementos). Ej. int *p; p = *dato; for (k = 0; k < 12; k++) printf(“%d”, *(p + k); //Para manejar filas y columnas, una forma equivalente //de recorrer los 12 elementos sería: for (i = 0; i < 3; i++) //i indica la fila. for (j = 0; j < 4; j++) //j indica la columna. printf(“%d”, *(p + i*4 + j) ); //Se usa i*4 para saltar 4 elementos cada vez que se //incremente una fila (cada fila tiene 4 elementos). 6. Punteros a funciones. Los punteros también pueden usarse para que apunten a la dirección de memoria donde comienza el código de una función, de forma que podrá realizarse una llamada a esa función a través de dicho puntero. Esto será explicado en detalle en un capítulo posterior. 7. Asignación Dinámica. Las variables que se declaran en un programa, como las que se han usado hasta ahora, ocupan memoria de forma estática. Esto implica que la variable se crea cuando comienza la ejecución del programa o la función donde esté declarada y se destruye cuando finaliza dicha ejecución. Durante todo ese tiempo las variables están ocupando memoria. Este modo de trabajar se denomina asignación estática de memoria. La memoria necesaria para almacenar los datos se reserva en tiempo de compilación. Existe otro modo de manejar la memoria para los datos que use un programa o una función, la asignación dinámica de memoria, que consiste en crear las variables en tiempo de ejecución, es decir que durante la ejecución del programa se va reservando la memoria necesaria para ir almacenando los datos. Además, también en tiempo de ejecución se puede liberar la memoria reservada de forma dinámica, cuando ya no se necesiten los datos almacenados en ella. Veamos las funciones que emplea C para crear variables de forma dinámica, en tiempo de ejecución: 11 - Función malloc: se emplea para reservar el tamaño de memoria que se indique, que quedará apuntado por un puntero. Se encuentra en stdlib.h. El formato que debe usarse es: tipo *p; p = (tipo *) malloc( nº bytes ); El campo nº bytes indica el número de bytes de memoria que se desea reservar, los cuales deben usarse para datos del mismo tipo, como int, char, float, etc. Por ello, en lugar del nº bytes se suele usar: tipo *p; p = (tipo *) malloc( nºdatos * sizeof(tipo) ); donde sizeof(tipo) devuelve el número de bytes que ocupa un tipo de datos determinado: sizeof(int) devuelve 2; sizeof(float) devuelve 4; sizeof(char) devuelve 1. Veamos unos ejemplos: float *p; p = (float *)malloc( 5 * sizeof(float)); //Reserva memoria para 5 variables float: 20 bytes. char *P; p = (char *)malloc(15 * sizeof(char)); //Reserva para 15 datos char: 15 bytes. Como puede verse, la función malloc devuelve un puntero al primer dato de los que haya reservado, excepto cuando se agota la memoria, en cuyo caso devuelve el valor NULL (puntero nulo, debe ir en mayúsculas). Esta situación debe ser comprobada cada vez que se use malloc, para asegurar la integridad del sistema. Para no cometer incompatibilidad de tipos, delante de malloc se colocará el tipo de puntero que queremos que nos devuelva, que debe coincidir con el tipo de puntero que se coloque a la izquierda de la asignación (=), p en los ejemplos anteriores. Ej.Teclear 1000 caracteres en una zona de memoria reservada de forma dinámica. int i; char *puntero; puntero = (char *) malloc( 1000 * sizeof(char) ); //puntero apunta a una zona de 1000 caracteres. if (puntero == NULL) { printf(“Memoria agotada”);exit(1);//Aborta programa } else { for (i=0; i < 1000; i++) //Teclear 1000 caracteres 12 *(puntero + i) = getche(); } Ej.Teclear 50 enteros creados dinámicamente. int i; int *ptr; if ((ptr = (int *) malloc (50 * sizeof(int))==NULL) { printf(“Memoria agotada”);exit(1);//Aborta programa } else { for (i=0; i<50; i++) { scanf(“%d”, ptr+i); fflush(stdin); } } Como se ve en estos ejemplos, se usará aritmética de punteros para ir recorriendo los datos de la memoria dinámica. No es aconsejable que el puntero que apunte al comienzo de la zona de memoria reservada deje de apuntar a esa posición. Si se desea tener un puntero que apunte a otra posición, debe declararse otro. - Función realloc: se emplea para modificar el tamaño de memoria reservado previamente de forma dinámica con malloc. Se encuentra en stdlib.h. El formato que debe usarse es: tipo *p, *q; p = (tipo *) malloc( nºbytes1 ); ... q = (tipo *) realloc(p, nºbytes2); El nºbytes2 será mayor o menor que nºbytes1, de este modo se modifica el número de bytes reservado por el malloc. El puntero q apuntará a la misma posición que p después del realloc, ambos apuntan a la misma dirección, por lo que p y q suelen ser la misma variable: p = (tipo *) realloc(p, nºbytes2); Los datos a los que apuntaba p antes del realloc no se pierden, quedando apuntados por q. También devuelve NULL cuando no hay memoria suficiente. - Función free: Se usa para liberar la memoria asignada con malloc. Se encuentra en stdlib.h. El formato que debe usarse es: tipo *p; p = (tipo *) malloc( nº bytes ); free(p); //p es el puntero usado en malloc. 13 Ej. char *puntero; puntero = (char *) malloc( 1000 * sizeof(char) ); ... free(puntero); Por tanto, con las funciones malloc y free se puede realizar la misma tarea que con realloc. Veamos un ejemplo. Ej. Ampliar una reserva de memoria de 100 enteros a 200 enteros sin utilizar realloc y sin perder los 100 enteros introducidos. int *p, *q; int i; p = (int *) malloc(100 * sizeof(int)); for (i=0; i < 100; i++) //Introduce 100 enteros. { scanf(“%d”, p+i); fflush(stdin); } q = p; //q apunta a la zona de 100 enteros. p = (int *) malloc(200 * sizeof(int)); for (i=0; i < 100; i++) *(p+i) = *(q+i); //Copia los 100 enteros cargados. free(q); //Libera la reserva inicial de 100 enteros. //Estas 5 últimas líneas de programa equivalen a: p = (int *)realloc(p, 200); 8. Problemas con Punteros. El uso de punteros tiene muchas ventajas y son necesarios para muchos programas, pero cuando contienen valores incorrectos, que no son los que el programador espera, provocan errores difíciles de detectar. Cuando se lee el dato al que apunta un puntero con valor erróneo, simplemente podemos obtener basura, un dato inesperado; pero cuando se escribe algún dato en la dirección de memoria donde apunta ese puntero erróneo, se puede estar escribiendo encima de otros datos e incluso encima de código de programa. Los errores más comunes con punteros son: - Un puntero no inicializado: almacenar un dato en la dirección donde apunta el puntero sin haberlo inicializado, es decir sin saber a donde está apuntando. Ej. int numero; int *ptr; numero = 10; 14 *ptr = numero; //Se almacena el 10 en una posición de //memoria desconocida. - Asignar un dato a un puntero: almacenar en el puntero un dato, no una dirección de memoria. Ej. int numero; int *ptr; numero = 10; ptr = numero; //10 es int, no dirección de memoria. //Debería usarse: ptr = &numero; - Suponer que las variables ocupan la memoria en el mismo orden en el que han sido declaradas en el programa: esos dos órdenes no coinciden: Ej. int primero[10], segundo[10]; int *ptr; int i; ptr = primero; for ( i = 0; i <= 19; i++) * (ptr++) = i; //i intenta recorrer los dos arrays seguidos, pero //esos dos arrays pueden no estar consecutivos en //memoria. - Incompatibilidad en los tipos: el tipo del dato y el del puntero no coinciden. Ej. float numero1; int *ptr; ptr = &numero1; //El puntero debe apuntar a dato int, //y el dato numero1 es float. 15 EJERCICIOS - CAPITULO 9: Realizar los siguientes algoritmos en pseudocódigo y en C: 1. Realizar la operación matemática tecleada, que puede ser sumar (+), restar (-), multiplicar (*), dividir (/) y tanto por ciento (%), sobre dos números reales tecleados. El programa finalizará cuando al solicitar operación se teclee ENTER o ESCAPE. Una vez declaradas las variables para guardar la operación y los dos números, deberán utilizarse punteros a dichas variables durante todo el programa. Al final visualizar las direcciones de memoria que están ocupando las variables. 2. Usando aritmética de punteros (no índices de arrays): a) Cargar en dos arrays dnis y sus edades, hasta que el dni sea 0; b) Visualizar la edad correspondiente al dni tecleado, hasta responder ‘N’ a “Otro DNI (S/N)?”. Deberá accederse a la variable que guarde el dni tecleado a través de un puntero a la misma. 3. Teclear un nombre y copiarlo en otro array, letra a letra, sin usar índices. 4. Usando aritmética de punteros: a) Cargar en un array 20 nombres; b) Pasar todos los nombres a mayúsculas; c) Visualizar la letra que ocupe la posición tecleada (columna) del nombre cuya fila sea también tecleada, hasta que la posición y fila tecleadas sean 0. 5. Usando un array de 5 punteros, donde cada uno apunta a una variable real independiente: a) Cargar 5 notas en esas variables; b) Visualizar la nota media de las notas aprobadas. 6. Usando el siguiente array de 13 punteros: char *mes[]={“MES INCORRECTO”,“ENERO”,..., “DICIEMBRE”}; visualizar el nombre del mes tecleando su número, de 1 a 12. Cuando el número tecleado sea –1 el programa finalizará. ¿Por qué si se declara el array del modo char mes[][15]={...} ocupa más memoria? 7. Usando un array de 4 punteros, donde cada uno apunta a un array de 10 números para guardar los 10 últimos movimientos de 4 cuentas: a) Cargar los 40 movimientos desde teclado; b) Consultar los 10 movimientos de la cuenta introducida (de 1 a 4), hasta que se teclee 0; c) Al final, visualizar el saldo de los 10 movimientos de cada cuenta y el saldo medio entre todas las cuentas. 8. Introducir nombres hasta que se teclee uno en blanco (máximo 100 nombres). Cada nombre, una vez tecleado, se debe copiar en una zona de memoria reservada dinámicamente con el tamaño exacto para guardar el nombre, usando un array de 100 punteros para apuntar a cada zona de memoria dinámica. Al final el programa permitirá consultar el nombre que se vaya tecleando, hasta responder ‘N’ a “Otra consulta (S/N)?”. 16 9. Programa que carga las notas de alumnos (máximo 30) usando un array de 30 punteros, hasta que se responda “N” a “Otro alumno (S/N)?”. Para cada alumno se tecleará cuántas notas se van a cargar y se reservará la memoria dinámica justa para ese número de notas de ese alumno. A continuación, se reservará una zona de memoria dinámica con el tamaño justo para cargar las edades de los alumnos tecleados previamente. Al final introducir número de alumno (de 1 a 30) y visualizar su nota media y edad, hasta que número de alumno sea 0. 10. Cargar nombres en arrays dinámicos con el tamaño justo para el nombre que va a almacenar cada uno, hasta que el nombre tecleado se deje en blanco (máximo 20 nombres). Una vez cargados los nombres, deberá guardarse en otro array dinámico la posición que le corresponde a cada nombre según el orden alfabético de menor a mayor. Finalmente visualizar los nombres ordenados de menor a mayor. 11. Manejar una cola de una consulta médica. Para ello se utilizará un puntero que apunte a un array dinámico que guardará los DNI de las personas que estén apuntadas a la cola. Este array debe tener en todo momento el tamaño justo para las personas que están en la cola. Debe crearse el siguiente menú: 1. Apuntarse a la cola (al final). 2. Llamar a consulta (borrar de la cola el primero, porque le toca). 3. Borrar de la cola (un DNI cualquiera desea borrarse de la cola). 17