El correcto entendimiento y uso de los punteros es critico para una fructífera programación en C. Hay tres razones fundamentales para decir esto. La primera es que los apuntadores proporcionan los medios por los cuales las funciones pueden modificar sus argumentos de llamada. Segundo, se utilizan para soportar las rutinas de asignación dinámica de C. Tercero, el uso de apuntadores puede mejorar la eficacia de la mayoría de las funciones, instrucciones y rutinas.
Ademas de ser una de las características mas poderosas, los punteros son también la característica mas peligrosa y poderosa de C. Por ejemplo, los no inicializados o apuntadores descontrolados, pueden provocar fallas en el sistema
Definición de apuntadores
Un apuntador es una variable que contiene una dirección de memoria. Normalmente, esa dirección es la posición de otra variable de memoria. Si una variable contiene la dirección de otra variable, entonces se dice que la primer variable "apunta" a la segunda.
Variables tipo apuntador
Si una variable va a contener un puntero, entonces tiene que declararse como tal. Una declaración de apuntador consiste en un tipo base, un * (asterisco) y el nombre de la variable. La forma general para declarar una variable apuntador es:
tipo *nombre ;
donde tipo es cualquier tipo valido en C y nombre es el nombre de la variable apuntador.
El tipo base del apuntador define el tipo de variables a las que puede apuntar. Técnicamente, cualquier tipo de apuntador puede apuntar a cualquier lugar de la memoria. Sin embargo, toda la aritmética de apuntadores esta hecha en relación a su tipo base, por lo que es importante declarar correctamente el apuntador.
Los operadores de apuntadores
Existen 2 operadores esenciales de apuntadores: & y *. El & es un operador monario que devuelve la dirección de memoria de su operando(hay que recordar que un operador monario solo necesita un operando). Por ejemplo
m = &cuenta
pone en m la dirección de la variable cuenta. Esta dirección es la posición interna de la variable en la computadora. La dirección no tiene ninguna relación con el valor cuenta. Se puede pensar en el operador & como devolviendo "la dirección de". Por tanto, la declaración de asignación anterior significaría "m recibe la dirección de cuenta".
Para entender mejor la asignación anterior, suponga que la variable cuenta utiliza la posición de memoria 2000 para guardar su valor. Entonces, despumes de la asignación anterior, m tiene el valor de 2000.
El segundo operador de punteros es el asterisco *, que es el complemento de &. Es también un operador monario que devuelve el valor de la variable localizada en la dirección que sigue. Por ejemplo, si m contiene la dirección de memoria de la variable cuenta, entonces
q = *m ;
pone el valor de cuenta en la variable q. Esta variable tendrá el valor de 100 porque ese es el valor que esta almacenado en la posición 2000, que es la dirección de memoria que se guardo en m. Se puede pensar en * como "en la dirección". En este caso, la sentencia anterior significa "q recibe el valor de la dirección m".
A veces, el hecho de que el operador * también signifique la multiplicación y & represente la operación Y a nivel de bits confunde a los que van empezando en la programación pero es importante resaltar que estos no tienen relación con los antes mencionados. Tanto & como * tienen mayor prioridad que todos los operadores aritméticos.
Las variables de tipo apuntador deben apuntar siempre al tipo de datos correcto. Por ejemplo, cuando se declara un apuntador de tipo int , el compilador asume que cualquier dirección que mantenga apunta a una variable entera. Debido a que C permite asignar cualquier dirección a una variable apuntado, el siguiente fragmento de código compila sin mensajes de error, pero no produce el resultado deseado.
void main (void)
{
float x,y;
int *p;
p = &x ;
y = *p;
}
Esto no asignara el valor de x a y. Debido a que p se declara como un puntero a entero, solo se transfieren 2 bytes de información a y, y no los bytes que normalmente forman un numero en coma flotante.
Inicializacion de apuntadores
Después de declarar un puntero pero antes de asignarle cualquier valor, este ya contiene otro valor desconocido. Si se intenta utilizar el apuntador antes de darle un valor, probablemente el programa fallara y no solo el programa, también podría fallar el sistema operativo.
Para evitar este tipo d errores,se recomienda siempre asignarle un valor al apuntador que se utilizara durante el programa.
Para poder entender como funcionan los apuntadores a funciones, se tiene que entender un poco como se compila y se llama a una función en C. Primeramente, cuando se compila cada función, el código fuente se transforma en un código objeto y se establece un punto de entrada. Cuando se llama a la función mientras se ejecuta el programa, se hace una llamada en lenguaje maquina a ese punto de entrada. Por tanto, si un apuntador contiene la dirección del punto de entrada a la función, puede utilizarse para llamar a la función.
La dirección de la función se obtiene utilizando el nombre de la función sin paréntesis ni argumentos, esto es parecido a la forma de obtener la dirección de un arreglo cuando se utiliza solo en nombre de este, sin indices.
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 evitar este tipo d errores,se recomienda siempre asignarle un valor al apuntador que se utilizara durante el programa.
Apuntadores y Funciones
Una característica algo confusa pero muy útil en C son los apuntadores a funciones. Incluso aunque una función no es una variable, tiene una posición física en memoria que se le puede asignar a un apuntador. La dirección de la función es el punto de entrada de dicha función, por lo que se puede usar un apuntador para llamar a la función.Para poder entender como funcionan los apuntadores a funciones, se tiene que entender un poco como se compila y se llama a una función en C. Primeramente, cuando se compila cada función, el código fuente se transforma en un código objeto y se establece un punto de entrada. Cuando se llama a la función mientras se ejecuta el programa, se hace una llamada en lenguaje maquina a ese punto de entrada. Por tanto, si un apuntador contiene la dirección del punto de entrada a la función, puede utilizarse para llamar a la función.
La dirección de la función se obtiene utilizando el nombre de la función sin paréntesis ni argumentos, esto es parecido a la forma de obtener la dirección de un arreglo cuando se utiliza solo en nombre de este, sin indices.
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.
Apuntadores y Arreglos
Los apuntadores pueden estructurarse en arreglos como cualquier otro tipo de dato. La declaración para un arreglo de apuntadores a enteros (int) de tamaño 10 es:
int *x[10];
Para asignar la dirección de una variable entera llamada var al tercer elemento del arreglo de apuntadores, se escribe
x[2] = &var ;
Para encontrar el valor de var, se escribe
*x[2]
Si se quiere pasar un arreglo de apuntadores a una función, se puede utilizar el mismo método que se utiliza para otros arreglos: llamar simplemente a la función con el nombre del arreglo sin indices. Por ejemplo, una función que reciba el arreglo x seria como esta:
void mostrar_arreglos (int *q[])
{
int t ;
for (t=0 ; t<10 ; t++)
printf ("%d", *q[t]);
}
Hay que recordar que la variable q no es un apuntador a enteros, sino un apuntador a un arreglo de apuntadores a enteros.
Un nombre de un arreglo es un índice a la dirección de comienzo del arreglo. Es decir, el nombre de un arreglo es un puntero al arreglo.
Sin embargo los apuntadores y los arreglos son diferentes:
Se puede hacer ap = a y ap++.
Hacer a = ap y a++ ES ILEGAL.
- Un apuntador es una variable.
Se puede hacer ap = a y ap++.
- Un arreglo NO ES una variable.
Hacer a = ap y a++ ES ILEGAL.
Apuntadores y Arreglos Multidimensionales
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 cuántas 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] ) { ..... }
f( int (*a)[35] ) { ..... }
En el último ejemplo se requieren los paréntesis (*a) ya que [ ] tiene una precedencia más alta que *.
int (*a)[35];
declara un apuntador a un arreglo de 35 enteros
Si hacemos a+2, nos estaremos refiriendo a la dirección del primer elemento que se encuentran en el tercer renglón de la matriz supuesta,
int *a[35]
declara un arreglo de 35 apuntadores a enteros.
Apuntadores y Cadenas
char *nomb[10];
char anomb[10][20];
En donde es válido hacer nomb[3][4] y anomb[3][4].
anomb es un arreglo verdadero de 200 elementos de dos dimensiones tipo char.
El acceso de los elementos anomb en memoria se hace bajo la
siguiente fórmula:
20*renglon + columna + dirección_base
En cambio 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).
CHAR *NOMB[10];
Tiene la ventaja de que cada apuntador puede apuntar a arreglos de diferente longitud.
Tiene la ventaja de que cada apuntador puede apuntar a arreglos de diferente longitud.
Apuntadores y Estructuras
Los apuntadores a estructuras se definen fácilmente y en una forma directa.
Un ejemplo de código de una relación de apuntadores y estructuras es la 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;
}
{
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;
}
Problemas con apuntadores
Nada puede dar mas problemas que un apuntador descontrolado. Los punteros son un arma de doble filo. Dan una potencia tremenda y son necesarios para muchos programas. Al mismo tiempo, cuando un apuntador accidentalmente contiene un valor erróneo, puede ser la falla mas difícil de detectar.
Un error de un apuntador erróneo es difícil de encontrar porque el problema no es el puntero en si mismo. El problema es que cada vez que se realiza una operación utilizando un apuntador, se esta leyendo o escribiendo en algún lugar desconocido de la memoria. Si se lee de el, lo peor que puede ocurrir es que se obtenga basura. Sin embargo, si se escribe en el, puede que se este escribiendo en otras partes de códigos o datos. Esto puede no evidenciarse hasta mas tarde en la ejecución del programa y puede llevar a buscar el error en un lugar incorrecto. Es posible que no haya nada que nos indique que el problema es el puntero
El error mas común cuando se trabaja con apuntadores es el puntero no inicializado . Aquí hay un ejemplo de un puntero no inicializado:
void mains (void)
{
int x, *p;
x = 10;
*p = x;
}
Este programa asigna el valor de 10 a alguna posición de la memoria desconocida. Como el apuntador p nunca ha recibido un valor, contiene basura. Este tipo de problema a menudo pasa inadvertido. La solución para este tipo de problemas es asegurarse siempre que el apuntador este apuntando a algo valido antes de utilizarlo.
El segundo erro mas común se produce por un desconocimiento del uso de apuntadores. Por ejemplo:
void main (void)
{
int x, *p;
x = 10 ;
p = x ;
printf ("%d", *p);
}
El segundo erro mas común se produce por un desconocimiento del uso de apuntadores. Por ejemplo:
void main (void)
{
int x, *p;
x = 10 ;
p = x ;
printf ("%d", *p);
}
La llamada a printf () no imprime el valor x en la pantalla, que es 10. Imprime algún valor desconocido porque la asignación
p = x ;
esta mal. La sentencia asigna el valor 10 al puntero p, que se supone contendría una dirección y no un valor. La corrección a este problema es escribir lo siguiente:
p = &x ;