Una característica esencial de la
programación en C es la existencia de punteros. Recuerdo que la
primera vez que me los explicaron me quedé con cara de “¿y esto
para qué sirve?”.
Pongamos que la memoria de un ordenador
se divide en celdas del mismo tamaño, como cajas de zapatos. Cada
caja tiene una etiqueta que la identifica, pongamos las letras de la
A a la Z (vamos a trabajar con un ordenador con muy poca memoria,
pero para el caso será sufciente) y dentro de esas cajas se guardan
los valores de las variables que vamos declarando.
(Aquí estamos haciendo una pequeña
trampa. En un ordenador todo son números, de modo que decir que
usamos letras para etiquetar las cajas es engañoso. En puridad las
etiquetas de las cajas deberían numerarse con 1, 2, 3, etc. pero eso
podría hacer algo difícil de entender las explicaciones que siguen.
Por ejemplo, es menos lioso decir que en la dirección de memoria
(caja de zapatos) “A” se almacena un 1, que decir que en la
dirección de meoria 1 se almacena un 1. De modo que me voy a
permitir la siguiente convención: para los números de las cajas de
zapatos vamos a usar el sistema “abecedarial”, en el cual la A
equivale al entero 1, la B al entero 2, etc. Es decir, A y 1 son la
misma cosa, pero usaremos el sistema abecedarial para las cajas de
zapatos y el natural para los valores que guardamos dentro de esas
cajas.)
También, y en aras de la simplicidad,
vamos a hacer que todas las cajas de zapatos sean del mismo tamaño,
y que en ellas sólo vamos a guardar números enteros.
En tal caso, si en un programa “a la
C” escribiéramos algo como esto:
int val;
val = 12;
val = val+2;;
En nuestra analogía lo que ocurriría
al ejecutar el código es:
- Hemos declarado la variable “val”, por lo que necesitaremos asignarle a esta variable una de las cajas de zapatos en la que guardar los valores que le vayamos asignando. Por simplificar vamos a poner que el valor de “val” irá en la caja de zapatos con la etiqueta “A”.
- Le asignamos a val el valor 12, de modo que en la caja A, que es la que hemos asignado a val, metemos un 12.
- Si ahora fuéramos a la caja de zapatos con la etiqueta “A” encontraríamos un 14.
La manera de saber cuál es la
dirección de memoria en la que se guarda el valor de “val” es
por medio del operador “&”. Es decir, podemos decir que
“&val” vale “A”.
Ahora vamos con los punteros. Los
punteros son variables que almacenan la dirección de otra variable,
es decir; las etiquetas con que identificamos las cajas de zapatos.
En el lenguaje C si queremos declarar un puntero a un entero (es
decir, una variable que almacene la dirección de memoria de un
entero) debemos escribir;
int *pVal;
Ahora mismo se dice que el puntero pVal
no apunta a ninguna parte, es decir, no le hemos dicho la dirección
de memoria de qué variable queremos que almacene (sería el caso
análogo a cuando declaramos una variable “normal” pero aún no
le hemos asignado un valor). O dicho de otro modo, aún no hemos
dicho a pVal a qué variable debe apuntar. Si queremos que pVal
almacene la dirección de memoria de val, debemos escribir:
pVal = &val;
El operador “&” se llama
operador “dirección” (“address” en inglés), y como hemos
comentado antes, nos dice la dirección de memoria en la que se está
almacenando una variable. Es decir, ahora el valor del puntero pVal
es por tanto “A”.
Hay otro operador fundamental cuando
trabajamos con punteros, el operador “*” o de “indirection”
(me niego a escribir “indirección”... ¡ups!), y nos da el valor
de la variable a la que apunta nuestro puntero. O en notación de
caja de zapatos: el operador “*” nos dice el contenido de la caja
de zapatos de la que nuestro puntero guarda la dirección. Es decir:
int val; // Los valores de val se van
a guardar en la caja de zapatos “A”
val = 12; // En la caja de zapatos A
metemos un doce
val = val+2; // Ahora hemos
cambiado el contenido de la caja A a 14
int *pVal; // Declaramos un puntero a
enteros
pVal = &val; // El valor de pVal
ahora es “A”, la caja en la que guardamos el valor de “val”
Entonces ahora *pVal vale 14; lo que
hay en la caja de zapatos con etiqueta “A” es un 14.
Aquí hay que tener en cuenta algo
importante. El operador “*” funciona en ambos sentidos. Es decir,
si ahora hacemos:
*pVal = 13;
Lo que hemos hecho es meter un 13 en la
caja de zapatos a la que apunta pVal, que es la A, y por lo tanto
ahora val valdrá 13. Es decir, por medio del puntero pVal hemos
podido cambiar de manera indirecta el valor de la variable val.
Y ahora era cuando venía mi cara de
“Todo estoy es muy interesante, pero ¿para qué sirve?”. Uno de
los ejemplos clásicos del uso de punteros es el de la creación en C
de una función “swap(val1,val2)” que simplemente intercambia el
valor de las variables val1 y val2. Es decir, si val1 vale 2 y val2
vale 5 entonces swap(val1, val2) hace que val1 pase a valer 5 y val2
pase a valer 2. La manera más inmediata en que se nos ocurriría
hacer esta función sería la siguiente:
#include <stdio.h> void swap(int i, int j) { int tmp = i; i = j; j = tmp; } int main(){ int val1 = 23; int val2 = 47; printf("Before. val1: %d, val2: %d\n", val1, val2); swap(val1, val2); printf("After. val1: %d, val2: %d\n", val1, val2); return 0; }
Sin embargo, si usáramos esta función para intercambiar el valor de
dos variables, nos llevaríamos la sorpresa de que no habría pasado
nada. El problema es que en C los argumentos se pasan a las funciones
“por valor”. Esto significa que cuando llamemos a una función,
se hacen copias de los argumentos, y dentro de la función se trabaja
con esas copias.
En notación de cajas de zapatos; si
usamos la variable val como argumento de una función, entonces se
mirará el contenido de la caja “A”, y se hará una copia de eso
en la caja “B” (o la primera caja que esté vacía). Dentro de la
función, cada vez que se haga algo sobre val, “en realidad” se
estará haciendo sobre el contenido de la caja “B”. De modo que
cuando la función haya terminado, cuando queramos volver a comprobar
el valor de val, veremos que no ha cambiado nada, ya que el contenido
de la caja A no se ha modificado durante todas las operaciones que se
han hecho dentro de la función.
Eso es exactamente lo que ocurre con la
función “swap” escrita arriba; su lógica es correcta, pero el
problema es que el intercambio de valores se hace entre las cajas de
zapatos que no nos interesan en este caso.
Por supuesto, ahora viene la pregunta,
¿entonces cómo se definiría una función swap que funcionase?
Bien, tenemos que pensar que lo que queremos cambiar es el contenido
de las cajas en las que se guardan los valores de “val1 ” y
“val2”, y para eso usaremos punteros. La idea de lo que debe
hacer la función es:
Tomar las direcciones de las cajas en
los que se guardan los valores de las variables val1 y val2, e
intercambia los valores que hay en dichas cajas.
Es decir, lor argumentos de la función
han de ser sendos punteros a las variables cuyos valores queremos
intercambiar, hacer el intercambio del contenido de las cajas a las
que hacen referencia, y ¡voi la!
#include <stdio.h> void swap(int *i, int *j) { int tmp = *i; *i = *j; *j = tmp; } int main(){ int val1 = 23; int val2 = 47; int *p1 = &val1; int *p2 = &val2; printf("Before. val1: %d, val2: %d\n", val1, val2); swap(p1, p2); printf("After. val1: %d, val2: %d\n", val1, val2); return 0; }
Ahora todo funciona correctamente. Sin embargo el código anterior
está un tanto “sobreactuado”. Si nos fijamos, no necesitamos
definir los punteros p1 y p2, y podemos llamar a la función swap
simplemente como swap(&val1, &val2), es decir, dando
directamente como argumentos las etiquetas de las cajas de zapatos en
las que se guardan los valores de las variables val1 y val2.