Punteros
| |
Introducción
Los punteros son una de las más potentes características de C, pero a la vez uno de sus mayores peligros. Los punteros nos permites acceder directamente a cualquier parte de la memoria. Esto da a los programas C una gran potencia. Sin embargo son una fuente ilimitada de errores. Un error usando un puntero puede bloquear el sistema (si usamos MS-DOS o Windows, no en Linux), y a veces puede ser difícil detectarlo. Otros lenguajes no nos dejan usar punteros para evitar estos problemas, pero a la vez nos quitan parte del control que tenemos en C.
-
Direcciones de variables
Al declarar una variable estamos diciendo al ordenador que reserve una parte de la memoria RAM para almacenarla. Cada vez que ejecutemos el programa la variable se almacenará en un sitio diferente; eso no lo podemos controlar; depende de la memoria disponible y de otros varios factores. Puede que se almacene en el mismo sitio, pero es mejor no fiarse. Dependiendo del tipo de variable que declaremos, el ordenador reservará más o menos memoria. Como vimos en la sección Variables cada tipo de variable ocupa más o menos bytes. Por ejemplo, si declaramos un char, el ordenador reserva 1 byte (8 bits). Cuando finaliza el programa todo el espacio reservado queda libre.
Existe una forma de saber qué direcciones ha reservado el ordenador. Se trata de usar el operador & (operador de dirección). Vamos a ver un ejemplo: definimos la variable a y obtenemos su valor y dirección.
#include |
Para mostrar la dirección de la variable usamos %p en lugar de %i. Sirve para escribir direcciones de punteros y variables. El valor se muestra en formato hexadecimal.
No hay que confundir el valor de la variable con la dirección donde está almacenada. La variable a está almacenada en un lugar determinado de la memoria y ese lugar no cambia mientras se ejecuta el programa. El valor de la variable puede cambiar a lo largo del programa, lo cambiamos a voluntad mediante el código. Ese valor está almacenado en la dirección de la variable. El identificador (nombre) de la variable es equivalente a poner un nombre a una zona de la memoria. Cuando en el programa escribimos a, en realidad estamos diciendo, "el valor que está almacenado en la dirección de memoria a la que llamamos a".
Qué son los punteros
Ahora ya estamos en condiciones de ver lo que es un puntero. Un puntero es una variable un tanto especial. Con un puntero podemos almacenar direcciones de memoria. En un puntero podemos tener guardada la dirección de una variable. Veamos la diferencia entre una variable puntero y las variables "normales".
En el dibujo anterior tenemos una representación de lo que puede ser la memoria del ordenador. Cada casilla representa un byte de la memoria. Y cada número es su dirección de memoria. La primera casilla es la posición 00001. La segunda casilla es la posición 00002, y así sucesivamente. Supongamos que ahora declaramos una variable: char numero = 43. El ordenador guardaría, por ejemplo, esta variable en la posición 00003. Esta posición de la memoria queda reservada y ya no la puede usar nadie más. Además, esta posición a partir de ahora se denomina numero. Como le hemos asignado el valor 43, el valor 43 se almacena en la posición de memoria 00003.
Si ahora usáramos el programa siguiente:
#include |
el resultado sería:
Dirección de numero = 00003, valor de numero = 43 |
Hemos dicho que un puntero sirve para almacenar la direcciones de memoria. Muchas veces los punteros se usan para guardar las direcciones de variables. Como cada tipo de variable ocupaba un espacio distinto, cuando declaramos un puntero debemos especificar el tipo de datos cuya dirección almacenará. En el próximo ejemplo queremos utilizar un puntero que almacene la dirección de una variable int. Así que para declararlo debemos hacer:
int *punt;
El * (asterisco) sirve para indicar que se trata de un puntero y debe ir justo antes del nombre de la variable, sin espacios. En la variable punt sólo se pueden guardar direcciones de memoria, no se pueden guardar datos. Vamos a volver sobre el ejemplo anterior un poco ampliado para ver cómo funciona un puntero:
#include |
Vamos a analizar línea a línea:
- En la primera, int numero;, reservamos memoria para numero (supongamos que queda como antes, en la posición 00003). Por ahora numero no tiene ningún valor.
- Siguiente línea: int *punt;, reservamos una posición de memoria para almacenar el puntero. Lo normal es que a medida que se declaran variables se guarden en posiciones contiguas. De modo que quedaría en la posición 00004. Por ahora punt no tiene ningún valor, es decir, no apunta a ninguna variable. Esto es lo que tenemos por ahora:
- Tercera línea: numero = 43;. Aquí estamos dando el valor 43 a numero. Se almacena 43 en la dirección 00003, que es la de numero.
- Cuarta línea: punt = №. Por fin damos un valor a punt. El valor que le damos es la dirección de numero (ya hemos visto que & devuelve la dirección de una variable). Así que punt tendrá como valor la dirección de numero, 00003. Por lo tanto tenemos:
Cuando un puntero tiene la dirección de una variable se dice que ese puntero apunta a esa variable. La declaración de un puntero depende del tipo de dato al que queramos apuntar. En general, la declaración es:
tipo_de_dato *nombre_del_puntero;
Para qué sirve un puntero y cómo se usa
Los punteros tienen muchas utilidades; por ejemplo, nos permiten pasar argumentos (o parámetros) a una función y modificarlos. También permiten el manejo de cadenas y de arrays. Otro uso importante es que nos permiten acceder directamente a la pantalla, al teclado y a todos los componentes del ordenador. Si sólo sirvieran para almacenar direcciones de memoria no serían de mucha utilidad. Nos deben dejar también la posibilidad de acceder al contenido de esas posiciones de memoria. Para ello se usa el operador * (operador de indirección), que no hay que confundir con el de la multiplicación.
#include |
Si nos fijamos en lo que ha cambiado con respecto al ejemplo anterior, vemos que para acceder al valor de numero usamos *punt. Esto es así porque punt apunta a numero y *punt nos permite acceder al valor al que apunta punt.
#include |
Ahora hemos cambiado el valor de numero a través de *punt. El resultado sería:
Dirección de numero = 00003, valor de numero = 30 |
En resumen, usando punt podemos apuntar a una variable y con *punt vemos o cambiamos el contenido de esa variable.
Un puntero no sólo sirve para apuntar a una variable, también sirve para apuntar una dirección de memoria determinada. Esto tiene muchas aplicaciones; por ejemplo nos permite controlar el hardware directamente (en MS-DOS y Windows, no en Linux). Podemos escribir directamente sobre la memoria de video y así escribir directamente en la pantalla sin usar printf.
8.2 Apuntadores y Funciones
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 */
}
8.3 Apuntadores y arreglos
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:
- Un apuntador es una variable. Se puede hacer
ap = ayap++. - Un arreglo NO ES una variable. Hacer
a = apya++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.