Capítulo 9. Punteros y asignación dinámica de memoria.

Anuncio
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 = №
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 = №
-
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
Descargar