Los apuntadores son una parte fundamental de C. Si usted no puede usar los apuntadores apropiadamente entonces esta perdiendo la potencia y la flexibilidad que C ofrece básicamente. El secreto para C esta en el uso de apuntadores.
C usa los apuntadores en forma extensiva. ¿Porqué?
C usa apuntadores explícitamente con:
C usa apuntadores explícitamente con:
Un apuntador es una variable que contiene la dirección en memoria de otra variable. Se pueden tener apuntadores a cualquier tipo de variable.
El operador unario o monádico &
devuelve la dirección de memoria de una variable.
El operador de indirección o dereferencia *
devuelve el ``contenido de un objeto apuntado por un apuntador''.
Para declarar un apuntador para una variable entera hacer:
int *apuntador;
Se debe asociar a cada apuntador un tipo particular. Por ejemplo, no se puede asignar la dirección de un short int
a un long int
.
Para tener una mejor idea, considerar el siguiente código:
main() { int x = 1, y = 2; int *ap; ap = &x; y = *ap; x = ap; *ap = 3; }
Cuando se compile el código se mostrará el siguiente mensaje:
warning: assignment makes integer from pointer without a cast
.
Con el objetivo de entender el comportamiento del código supongamos que la variable x
esta en la localidad de la memoria 100
, y
en 200
y ap
en 1000
. Nota: un apuntador es una variable, por lo tanto, sus valores necesitan ser guardados en algún lado.
int x = 1, y = 2; int *ap; ap = &x;
100 | 200 | 1000 | |||
x | 1 | y | 2 | ap | 100 |
Las variables x
e y
son declaradas e inicializadas con 1
y 2
respectivamente, ap
es declarado como un apuntador a entero
y se le asigna la dirección de x
(&x
). Por lo que ap
se
carga con el valor 100
.
y = *ap;
100 | 200 | 1000 | |||
x | 1 | y | 1 | ap | 100 |
Después y
obtiene el contenido de ap
. En el ejemplo ap
apunta a la localidad de memoria 100 -- la localidad de x
. Por lo
tanto, y
obtiene el valor de x
-- el cual es 1.
x = ap;
100 | 200 | 1000 | |||
x | 100 | y | 1 | ap | 100 |
Como se ha visto C no es muy estricto en la asignación de valores de
diferente tipo (apuntador a entero). Así que es perfectamente legal
(aunque el compilador genera un aviso de cuidado) asigna el valor
actual de ap
a la variable x
. El valor de ap
en ese momento es 100
.
*ap = 3;
100 | 200 | 1000 | |||
x | 3 | y | 1 | ap | 100 |
Finalmente se asigna un valor al contenido de un apuntador (*ap
).
Importante: Cuando un apuntador es declarado apunta a algún lado. Se debe inicializar el apuntador antes de usarlo. Por lo que:
main() { int *ap; *ap = 100; }
puede generar un error en tiempo de ejecución o presentar un comportamiento errático.
El uso correcto será:
main() { int *ap; int x; ap = &x; *ap = 100; }
Con los apuntadores se puede realizar también aritmética entera, por ejemplo:
main() { float *flp, *flq; *flp = *flp + 10; ++*flp; (*flp)++; flq = flp; }
NOTA: Un apuntador a cualquier tipo de variables es una dirección en memoria -- la cual es una dirección entera, pero un apuntador NO es un entero.
La razón por la cual se asocia un apuntador a un tipo de dato, es por que se debe conocer en cuantos bytes esta guardado el dato. De tal forma, que cuando se incrementa un apuntador, se incrementa el apuntador por un ``bloque'' de memoria, en donde el bloque esta en función del tamaño del dato.
Por lo tanto para un apuntador a un char, se agrega un byte a la dirección y para un apuntador a entero o a flotante se agregan 4 bytes. De esta forma si a un apuntador a flotante se le suman 2, el apuntador entonces se mueve dos posiciones float que equivalen a 8 bytes.
Cuando C pasa argumentos a funciones, los pasa por valor, es decir, si el parámetro es modificado dentro de la función, una vez que termina la función el valor pasado de la variable permanece inalterado.
Hay muchos casos que se quiere alterar el argumento pasado a la función y recibir el nuevo valor una vez que la función ha terminado. Para hacer lo anterior se debe usar una llamada por referencia, en C se puede simular pasando un puntero al argumento. Con esto se provoca que la computadora pase la dirección del argumento a la función.
Para entender mejor lo anterior consideremos la función swap()
que
intercambia el valor de dos argumentos enteros:
void swap(int *px, int *py); main() { int x, y; x = 10; y = 20; printf("x=%d\ty=%d\n",x,y); swap(&x, &y); printf("x=%d\ty=%d\n",x,y); } void swap(int *px, int *py) { int temp; temp = *px; /* guarda el valor de la direccion x */ *px = *py; /* pone y en x */ *py = temp; /* pone x en y */ }
Existe una relación estrecha entre los punteros y los arreglos. En C, un nombre de un arreglo es un índice a la dirección de comienzo del arreglo. En esencia, el nombre de un arreglo es un puntero al arreglo. Considerar lo siguiente:
int a[10], x; int *ap; ap = &a[0]; /* ap apunta a la direccion de a[0] */ x = *ap; /* A x se le asigna el contenido de ap (a[0] en este caso) */ *(ap + 1) = 100; /* Se asigna al segundo elemento de 'a' el valor 100 usando ap*/
Como se puede observar en el ejemplo la sentencia a[t] es idéntica a ap+t. Se debe tener cuidado ya que C no hace una revisión de los límites del arreglo, por lo que se puede ir fácilmente más alla del arreglo en memoria y sobreescribir otras cosas.
C sin embargo es mucho más sútil en su relación entre arreglos y apuntadores. Por ejemplo se puede teclear solamente:
ap = a;
en vez deap = &a[0];
y también*(a + i)
en vez dea[i]
, esto es,&a[i]
es equivalente cona+i
.
Y como se ve en el ejemplo, el direccionamiento de apuntadores se puede expresar como:
a[i]
que es equivalente a*(ap + i)
Sin embargo los apuntadores y los arreglos son diferentes:
ap = a
y
ap++
.
a = ap
y a++
ES
ILEGAL.
Este parte es muy importante, asegúrese haberla entendido.
Con lo comentado se puede entender como los arreglos son pasados a las funciones. Cuando un arreglo es pasado a una función lo que en realidad se le esta pasando es la localidad de su elemento inicial en memoria.
Por lo tanto:
strlen(s)
es equivalente astrlen(&s[0])
Esta es la razón por la cual se declara la función como:
int strlen(char s[]);
y una declaración equivalente esint strlen(char *s);
ya que char s[]
es igual que char *s
.
La función strlen() es una función de la biblioteca estándar que regresa la longitud de una cadena. Se muestra enseguida la versión de esta función que podría escribirse:
int strlen(char *s) { char *p = s; while ( *p != '\0' ) p++; return p - s; }
Se muestra enseguida una función para copiar una cadena en otra. Al igual que en el ejercicio anterior existe en la biblioteca estándar una función que hace lo mismo.
void strcpy(char *s, char *t) { while ( (*s++ = *t++) != '\0' ); }
En los dos últimos ejemplos se emplean apuntadores y asignación por valor.
Nota: Se emplea el uso del caracter nulo con la sentencia while
para
encontrar el fin de la cadena.
En C se pueden tener arreglos de apuntadores ya que los apuntadores son variables.
A continuación se muestra un ejemplo de su uso: ordenar las líneas de un texto de diferente longitud.
Los arreglos de apuntadores son una representación de datos que manejan de una forma eficiente y conveniente líneas de texto de longitud variable.
¿Cómo se puede hacer lo anterior?
char
grande.
Observando que \n
marca el fin de cada línea. Ver figura 8.1.
strcmp()
.
Con lo anterior se elimina:
Un arreglo multidimensional puede ser visto en varias formas en C, por ejemplo:
Un arreglo de dos dimensiones es un arreglo de una dimensión, donde cada uno de los elementos es en sí mismo un arreglo.
Por lo tanto, la notación
a[n][m]
nos indica que los elementos del arreglo están guardados renglón por renglón.
Cuando se pasa una arreglo bidimensional a una función se debe especificar el número de columnas -- el número de renglones es irrelevante.
La razón de lo anterior, es nuevamente los apuntadores. C requiere conocer cuantas son las columnas para que pueda brincar de renglón en renglón en la memoria.
Considerando que una función deba recibir int a[5][35]
, se puede
declarar el argumento de la función como:
f( int a[][35] ) { ..... }
o aún
f( int (*a)[35] ) { ..... }
En el último ejemplo se requieren los parénteis (*a)
ya que [ ]
tiene una precedencia más alta que *
.
Por lo tanto:
int (*a)[35];
declara un apuntador a un arreglo de 35 enteros, y por ejemplo si hacemos la siguiente referenciaa+2
, nos estaremos refiriendo a la dirección del primer elemento que se encuentran en el tercer renglón de la matriz supuesta, mientras que
int *a[35];
declara un arreglo de 35 apuntadores a enteros.
Ahora veamos la diferencia (sutil) entre apuntadores y arreglos. El manejo de cadenas es una aplicación común de esto.
Considera:
char *nomb[10]; char anomb[10][20];
En donde es válido hacer nomb[3][4]
y anomb[3][4]
en C.
Sin embargo:
anomb
es un arreglo verdadero de 200 elementos de dos
dimensiones tipo char
.
anomb
en memoria se hace bajo la
siguiente fórmula 20*renglon + columna + dirección_base
nomb
tiene 10 apuntadores a elementos.
NOTA: si cada apuntador en nomb
indica un arreglo de 20
elementos entonces y solamente entonces 200 chars estarán disponibles (10
elementos).
Con el primer tipo de declaración se tiene la ventaja de que cada apuntador puede apuntar a arreglos de diferente longitud.
Considerar:
char *nomb[] = { "No mes", "Ene", "Feb", "Mar", .... }; char anomb[][15] = { "No mes", "Ene", "Feb", "Mar", ... };
Lo cual gráficamente se muestra en la figura 8.2. Se puede indicar que se hace un manejo más eficiente del espacio haciendo uso de un arreglo de apuntadores y usando un arreglo bidimensional.
La inicialización de arreglos de apuntadores es una aplicación ideal para un arreglo estático interno, por ejemplo:
func_cualquiera() { static char *nomb[] = { "No mes", "Ene", "Feb", "Mar", .... }; }
Recordando que con el especificador de almacenamiento de clase static se reserva en forma permanente memoria el arreglo, mientras el código se esta ejecutando.
Los apuntadores a estructuras se definen fácilmente y en una forma directa. Considerar lo siguiente:
main() { struct COORD { float x,y,z; } punto; struct COORD *ap_punto; punto.x = punto.y = punto.z = 1; ap_punto = &punto; /* Se asigna punto al apuntador */ ap_punto->x++; /* Con el operador -> se accesan los miembros */ ap_punto->y+=2; /* de la estructura apuntados por ap_punto */ ap_punto->z=3; }
Otro ejemplo son las listas ligadas:
typedef struct { int valor; struct ELEMENTO *sig; } ELEMENTO; ELEMENTO n1, n2; n1.sig = &n2;
La asignación que se hace corresponde a la figura 8.3
Nota: Solamente se puede declarar sig
como un apuntador tipo
ELEMENTO
. No se puede tener un elemento del tipo variable ya que esto
generaría una definición recursiva la cual no esta permitida. Se
permite poner una referencia a un apuntador ya que los los bytes se dejan de
lado para cualquier apuntador.
A continuación se muestran dos errores comunes que se hacen con los apuntadores.
int *x *x = 100;
lo adecuado será, tener primeramente una localidad física de memoria,
digamos int y;
int *x, y; x = &y; *x = 100;
Supongamos que se tiene una función llamada malloc()
la cual trata de
asignar memoria dinámicamente (en tiempo de ejecución), la cual regresa un
apuntador al bloque de memoria requerida si se pudo o un apuntador a nulo en
otro caso.
char *malloc()
-- una función de la biblioteca estándar que se
verá más adelante.
Supongamos que se tiene un apuntador char *p
Considerar:
*p = (char *) malloc(100): /* pide 100 bytes de la memoria */ *p = 'y';
Existe un error en el código anterior. ¿Cuál es?
El *
en la primera línea ya que malloc
regresa un apuntador
y *p
no apunta a ninguna dirección.
El código correcto deberá ser:
p = (char *) malloc(100);
Ahora si malloc
no puede regresar un bloque de memoria, entonces
p
es nulo, y por lo tanto no se podrá hacer:
*p = 'y';
Un buen programa en C debe revisar lo anterior, por lo que el código anterior puede ser reescrito como:
p = (char *) malloc(100): /* pide 100 bytes de la memoria */ if ( p == NULL ) { printf("Error: fuera de memoria\n"); exit(1); } *p = 'y';
Leer los datos de la entrada estándar. La primera línea es una sola palabra, en la segunda línea se tiene un texto general. Leer ambas hasta encontrar un caracter de nueva línea. Recordar que se debe insertar un caracter nulo antes de procesar.
La salida típica podría ser:
La palabra es "el" La sentencia es "el perro, el gato y el canario" La palabra ocurrio 3 veces.