El uso de punteros en C sólo se justifica si ofrecen una clara ventaja frente a la manipulación de datos directamente. A continuación se describen dos situaciones en las que el uso de punteros redunda en código más eficiente, es decir, que ejecuta a una mayor velocidad, o que ocupa un menor espacio en memoria.
Hay situaciones en que nos interesará crear funciones que modificen el valor de la variable que se le pasa como parámetro y que esta modificación retorne a la función llamadora. En ese caso, decimos que los parámetros se pasan por referencia. Cuando se pasa una variable por referencia, el compilador no pasa la copia de un valor del argumento (como hemos visto en la sección de funciones); sinó que pasa una refrencia que indica a la función dónde se encuentra la variable en memoria. Por tanto, al pasar la dirección de memoria, cuando se hacen cambios sobre esa variable en la función se estará variando el valor de la variable y no el de su copia. Un ejemplo muy claro de función que pasa valores por referencia es el de la función "swap" o "intercambiador". La función "swap" recibe dos parámetros e intercambia el valor del uno por el otro. Observa el código que iplementa esta función.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <stdio.h> /* Definición de función "swap". Fíjate que las variables se reciben como puntero a esas variables. */ void sswap ( int *x, int *y ) { /*Declaramos una variable temporal*/ int tmp; tmp = *x; *x = *y; *y = tmp; } int main() { int a, b; a = 1; b = 2; /*Llamamos a la función "swap" pasándole la dirección a las variables a y b.*/ swap( &a, &b ); /*Imprime los valores de a y b intercambiados*/ printf(" a = %d b = %d\n", a, b ); } |
Si copias el código y lo ejecutas verás que el programa imrprime los valores de a y b intercambiados. Esto ocurre porque la función, en vez de recibir los parámetros por valor, los recibe por referencia, es decir, recibe su dirección de memoria. Cuando, dentro de la función, se alteran los valores de a y b, se accede directamente al contenido de la dirección de memoria. Por esa razón, fuera del ámbito de la función, a y b mantienen los cambios que han sufrido en la función.
Modifica el programa anteror sin usar indirecciones. Verás que, cuando salgas de la función los valores de a y b serán los mismo que al inicio, sin haberse intercambiado.
Cuando se llama a una función en C, los valores de los parámetros se copian desde el ámbito llamador (lugar en el que está la llamada a la función) al ámbito llamado (el cuerpo de la función). El siguiente programa muestra la ejecución de una función que recibe como parámetro dos estructuras.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | #include <stdio.h> #include <math.h> /* Definición de la estructura */ struct coordenadas { float x; float y; float z; }; /* Definición de función que calcula la distancia entre dos puntos */ float distancia(struct coordenadas a, struct coordenadas b) { return sqrtf(pow(a.x - b.x, 2.0) + pow(a.y - b.y, 2.0) + pow(a.z - b.z, 2.0)); } int main() { /* Declaración e inicialización de dos variables */ struct coordenadas punto_a = { 3.5e-120, 2.5, 1.5 }; struct coordenadas punto_b = { 5.3e-120, 3.1, 6.3 }; float d; /* Almacenar el resultado */ /* Llamada a la función con las dos estructuras */ d = distancia(punto_a, punto_b); /* Imprimir el resultado */ printf("%f\n", d); return 0; } |
En la línea 26 se realiza una copia de las estructuras
punto_a
y punto_b
en el ámbito de la función
main
a las estructuras a
y b
en el
ámbito de la función distancia
. Esta copia tiene dos efectos:
el programa invierte tanto tiempo como datos haya en la estructura, y
durante la ejecución de la función se mantienen en memoria dos copias
completas de las estructuras. El uso de punteros ofrece una alternativa
más eficiente. La función puede reescribirse para que reciba dos
punteros a las estructuras. La definición de la
función pasa a ser:
float distancia(struct coordenadas *a_ptr, struct coordenadas *b_ptr)
{
return sqrtf(pow(a_ptr->x - b_ptr->x, 2.0) +
pow(a_ptr->y - b_ptr->y, 2.0) +
pow(a_ptr->z - b_ptr->z, 2.0));
}
La llamada a la función en la línea 26 pasa ahora a ser
d = distancia(&punto_a, &punto_b);
Con esta nueva versión, el programa ejecuta exactamente los mismos cálculos, obtiene el mismo resultado, pero su ejecución es más rápida, y utiliza menos memoria.
El programa del ejemplo anterior utiliza las funciones
de biblioteca “pow
” y
“sqrtf
”. En una ventana con el intérprete de
comandos utiliza el comando man seguido de cada uno
de estos nombres para averiguar qué hacen. Para utilizar estas funciones
se debe incluir el fichero math.h
mediante la
directiva en la línea 2. Además, la biblioteca de funciones matemáticas
ha de ser incluida explícitamente al compilar mediante la opción
-lm en el comando
gcc -Wall -o programa fichero.c -lm
en el que debes reemplazar
fichero.c
con el nombre de tu fichero.
Modifica el programa del ejemplo para incrementar la precisión en los cálculos. Compara los dos resultados obtenidos.
Otro escenario en el que los punteros son de gran utilidad es para “enlazar” estructuras de datos. Supongamos que en la agenda de tu teléfono móvil puedes guardar para cada contacto un mapa. Una posible estructura de datos almacenaría los datos del contacto y tendría un campo para almacenar el mapa. A continuación se muestra una posible definición de las estructuras de datos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #define SIZE 64 struct datos_mapa { /* Datos referentes al mapa */ ... }; struct datos_contacto { char nombre[SIZE]; char apellidos[SIZE]; ... /* Datos del mapa para este contacto */ struct datos_mapa mapa; }; |
Si la agenda almacena 100 contactos, cada uno de ellos tiene el espacio para almacenar los datos del mapa. Pero imaginemos que este mapa ocupa mucho espacio en memoria debido a los gráficos, y que varios usuarios tienen el mismo mapa, por lo que largas porciones de memoria son réplicas. Lo ideal es mantener la posibilidad de tener un mapa por contacto, pero a la vez evitar tener mapas duplicados. Una posible solución es guardar por separado los usuarios y los mapas. En la estructura del usuario se “enlaza” el mapa pertinente. De esta forma, si varios usuarios comparten el mismo mapa, todos estarán enlazados a una única copia. Este enlace se puede implementar con el uso de punteros tal y como se muestra en las siguientes definiciones:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | #define SIZE 64
struct datos_mapa
{
/* Datos referentes al mapa */
...
};
struct datos_contacto
{
char nombre[SIZE];
char apellidos[SIZE];
...
/* Enlace a los datos del mapa */
struct datos_mapa *mapa;
}; |
En la siguiente figura se muestra la diferencia en tamaño de memoria utilizado en las dos soluciones para el caso en que cinco contactos comparten dos mapas.
Con la nueva solución, el acceso a los datos del mapa se
debe hacer con el operador de indirección
“->
” porque el campo de los datos del mapa es
un puntero.