Introducción a la programación

Antes de explicar qu� son, para qu� sirven y c�mo hacer uso de ellos, vamos primero a exponer unas cuantas consideraciones:

Los punteros no est�n disponibles en todos los lenguajes de programaci�n, ello significa que si, por ejemplo, programais en C, podeis utilizarlos, pero si programais en Basic no.

Su uso produce unas ciertas ventajas sobre los programas, y es que aumentan la flexibilidad a la hora de, por ejemplo, declarar vectores o matrices cuyo tama�o no sea predefinido. No acaban ah� las ventajas, con estructuras de datos m�s complejas son el pan nuestro de cada d�a, pero de todo eso ya hablaremos.

Sin embargo, no todo es maravilloso, y tienen serios inconvenientes que provocan m�s de un quebradero de cabeza. Es muy f�cil equivocarse us�ndolos y con ello escribir en alguna zona de memoria no aconsejable (como las reservadas por el sistema operativo), lo que provoca el consiguiente cuelgue en sistemas poco robustos que permitan escribir en cualquier parte. No hay nada m�s peligroso que un puntero incontrolado o sin inicializar.

Vistas las pegas (ya las descubrireis cuando el programa se os cuelgue sin motivo aparente), vamos a ver qu� es eso de los punteros, pues parece que tenemos que tenerles un cierto respeto :-D

.�Qu� es un puntero

Intuitivamente, un puntero es una flecha que apunta a alguna parte. �A qu� parte? Obviamente, si estamos hablando de ordenadores, apuntar� a una cierta direcci�n de memoria. Es decir, un puntero es una representaci�n simb�lica de una direcci�n de memoria.

(Nota importante: voy a usar la notaci�n de C para los punteros)

Veamos un ejemplo; supongamos que tenemos declarada la variable Variable_Misteriosa en nuestro programa. Si queremos saber cu�l es la direcci�n de dicha variable, pondremos lo siguiente:

  &Variable_Misteriosa
  { el s�mbolo & precediendo a una variable es el
    operador direcci�n de memoria } 

y si, por ejemplo, escribimos en nuestro programa:

Mostrar_por_Pantalla(&Variable_Misteriosa)

saldr� en pantalla (por decir algo):

56743

que no es m�s que la celdilla de la memoria en la que se almacena el valor que nosotros identificamos con Variable_Misteriosa. Es decir, si hemos hecho previamente (suponiendo que fuera una variable entera):

Variable_Misteriosa <- 9

lo que significa es que en la direcci�n de memoria 56743 est� almacenado el valor 9. �Est� todo claro hasta aqu�? Bien, pues sigamos :)

En el caso que he puesto como ejemplo, lo que ten�amos era una *constante* puntero. Pero tambi�n podemos declarar como variables, variables de tipo puntero. Estas variables contendr�n, como hemos dicho, una direcci�n de memoria.

Por ejemplo, si tenemos una variable puntero que se llame Mi_Puntero, y una variable normal que se llame Mi_Variable, podemos hacer cosas como esta:

Mi_Puntero <- &Mi_Variable

con lo que en Mi_Puntero tenemos almacenada la direcci�n de memoria de Mi_Variable (y decimos que Mi_Puntero APUNTA a Mi_Variable). Y aqu� surge el primer problema. Si hacemos:

Mi_Puntero <- Mi_Variable

en Mi_Puntero est� almacenada, como direcci�n de memoria, el valor de la variable Mi_Variable. Si luego hemos de escribir algo en la zona de memoria a la que apunta Mi_Puntero, ya la hemos liado, puesto que no sabemos qu� puede haber ah�.

Resumiendo: no es lo mismo la direcci�n de memoria de una variable que el contenido de la variable (o, lo que es lo mismo, el contenido de esa direcci�n de memoria).

Seguimos. Como hemos dicho que Mi_Puntero es una variable de tipo puntero, podemos hacer, m�s adelante en el curso del programa, que apunte a otra variable de la misma forma.

Ya sabemos que Mi_Puntero apunta a Mi_Variable. En este caso, podemos utilizar el operador de indirecci�n * (tambi�n es de C; no confundirlo con el operador de multiplicaci�n) para encontrar el valor almacenado en Mi_Variable. �C�mo? Escribiendo (por ejemplo):

 Mi_Puntero <- &Mi_Variable
 Mostrar_por_Pantalla(*Mi_Puntero) 

es exactamente lo mismo que hacer:

Mostrar_por_Pantalla(Mi_Variable)

Lo que hemos hecho ha sido apuntar a Mi_Variable con Mi_Puntero (es decir, Mi_Puntero contiene la direcci�n de memoria de Mi_Variable) y despu�s, con el operador *, mostrar el CONTENIDO de lo que hay en la direcci�n de memoria que guarda Mi_Puntero.

Es decir, lo que hace el operador de indirecci�n es, seguido de un puntero, dar el valor almacenado en la direcci�n de memoria a la que apunta el puntero.

S� que todo esto es un trabalenguas de cuidado, as� que releed con cuidado la primera parte del texto y seguidle bien la pista. Haced alg�n dibujo si eso os ayuda.

Mientras, yo prosigo con el punto siguiente:

.�Declaraci�n de punteros

He dicho antes que podemos tener variables de tipo puntero, as� que, lo l�gico es querer saber c�mo declararlas, y a eso es a lo que vamos.

Cuando decl�rabamos (hace ya mucho tiempo) variables de tipo entero, pon�amos:

a,b,c : ENTEROS

�No podr�amos poner para los punteros algo como:?

a,b,c: PUNTEROS

Bien, pues la respuesta es NO. �Y por qu� no? Pues porque para declarar un puntero necesitamos saber, aparte de que va a apuntar a alguien, a qu� TIPO de alguien va a apuntar, es decir, a qu� tipo de variable va a apuntar. No ser� lo mismo apuntar a un entero que a un caracter o que a un real, pues estos tipos ocupan distintos tama�os en memoria, y eso es algo fundamental para otra cosa que veremos m�s adelante, la aritm�tica de punteros.

Para declarar una variable de tipo puntero a un tipo de dato, lo haremos como sigue:

  *Puntero1            : CARACTER
  *Puntero2            : ENTERO
  *Puntero3, *Puntero4 : REAL 

(esto en C lo har�amos as�: char *Puntero1; int *Puntero2; float *Puntero3, *Puntero4 ;)

y tenemos que Puntero1 ser� una variable puntero que apunte a una variable de tipo caracter, Puntero2 apuntar� a una variable de tipo entero y Puntero3 y Puntero4 apuntar�n a variables de tipo real.

Ahora que los tenemos declarados, suponiendo que tengamos las variables Var1 de tipo caracter, Var2 de tipo entero y Var3 de tipo real, podemos inicializarlos haciendo lo que ya hemos visto:

  Puntero1 <- &Var1
  Puntero2 <- &Var2
  Puntero3 <- &Var3

y acceder a sus contenidos escribiendo:

  *Puntero1
  *Puntero2
  *Puntero3

sin embargo, si se nos ocurre usar en el programa:

*Puntero4

puede pasar de todo. �Por qu�? Pues porque este puntero no est� inicializado (lo he dejado a drede). Eso significa que, en principio, puede contener cualquier cosa. Al ser un puntero, esa ''cualquier cosa'' ser� interpretada como una direcci�n de memoria, y el contenido de ''cualquier direcci�n de memoria'' puede ser de lo m�s ins�lito, lo que no es muy recomendado si, por ejemplo, estamos haciendo c�lculos.

Una cosa que no hay que perder de vista es que los punteros, al ser variables, tienen una posici�n de memoria en la que se guarda su contenido. Es decir, si hacemos:

&Puntero1

estamos accediendo a la direcci�n de memoria del puntero Puntero1. Y el contenido de esa direcci�n de memoria es la direcci�n de memoria de la variable a la que apunta.

.�Aritm�tica de Punteros

Trabajando con punteros, �qu� sentido tiene hacer:?

Puntero <- Puntero + 1

Pues eso depende de a qu� tipo de variable est� apuntando la variable Puntero. Si Puntero apunta a un car�cter, como un car�cter ocupa 1 byte en memoria, al hacer la operaci�n anterior, Puntero est� apuntando al byte siguiente al que apuntaba antes. Si la operaci�n que hacemos es:

Puntero <- Puntero - 1

lo que hace es apuntar al byte anterior. Sin embargo, si Puntero apunta a un real, como un real ocupa en memoria 10 bytes, en este caso apunta a los 10 bytes anteriores al que estaba apuntando.

En otras palabras, cuando sumamos 1 a un puntero, no estamos dici�ndole que apunte a la direcci�n siguiente, sino que pase a apuntar a la siguiente celdilla de memoria de acuerdo con el tipo base al que apunta.

Y si en vez de sumar (o restar) 1, sumamos (o restamos) N, avanzamos N veces lo que ocupe el tipo de variable al que estamos apuntando.

Adem�s, tambi�n podemos sumar y restar punteros. Veamos unos ejemplos para aclararlo. Supongamos que Puntero1 y Puntero2 son punteros a enteros, y hacemos:

Puntero2 <- Puntero1 + 4

entonces, Puntero2 apunta a la posici�n de memoria 8 bytes posterior a la que apunta Puntero1. Y si hacemos:

Puntero2 <- Puntero1 - 1

Puntero2 apunta a la posici�n de memoria 2 bytes anterior a la que apunta Puntero1. Tambi�n podemos hacer:

i <- Puntero2 - Puntero1

Esto normalmente se hace dentro de un mismo array, para saber cu�ntos elementos los separan. Notar que el resultado que da no es en bytes, sino en las mismas unidades que el tama�o del tipo del array. Este resultado debe ser asignado a una variable de tipo entero (la variable i que aparece en el ejemplo se presupone previamente declarada).

.��Y todo esto para qu� sirve?

Hablar de punteros por hablar puede ser muy entretenido si no se tiene nada mejor que hacer, pero resulta que yo he dicho que son muy �tiles y no lo he dicho gratuitamente.

Por ejemplo, ahora que sabemos lo que son los punteros, vamos a ver que est�n relacionados con el paso de par�metros en las funciones, con lo que hablaremos de paso de variables por valor y por referencia.

En su d�a hablamos de funciones y de procedimientos, y dijimos que, en el caso de las funciones, pas�bamos una serie de variables que no eran modificadas, y se nos devolv�a un �nico valor. Por contra, en los procedimientos pas�bamos una serie de variables que pod�an ser modificadas pero, a cambio, no se nos devolv�a ning�n valor.

Esto es as� en lenguajes como el Pascal, pero en C esto no sucede. Quiero decir, que en C no tenemos funciones y procedimientos, sino que s�lo tenemos funciones. Las funciones siguen teniendo la caracter�stica de que devuelven un �nico valor, y que las variables que le pasamos como argumentos no pueden ser modificadas. �Y si necesito que se me devuelvan dos valores? �C�mo lo hago, si el C s�lo me permite funciones?

Pues lo hago con lo que se llama ''paso de par�metros por referencia''.

En primer lugar, he de decir que en C, cuando pasamos variables a las funciones, lo que hacemos es un ''paso de par�metros por valor''. Es decir, la funci�n recibe los valores de las variables, pero no sabe nada m�s de ellas.

Cuando hacemos un paso por referencia, lo que estamos pasando a la funci�n no es el valor de la variable, sino la direcci�n de memoria. Y la funci�n, al tener la direcci�n de memoria de la variable, ya s� puede modificarla, pudiendo, en cierto sentido, ''devolvernos'' varios valores, solucionando el problema que ten�amos al no disponer de procedimientos.

Voy a poner un ejemplo de esto comentado para que se entienda la estructura y el por qu�. Y para ello, voy a utilizar un ejercicio que ya propuse, el de escribir un procedimiento que intercambie el valor de dos variables. El algoritmo de este procedimiento es algo tan sencillo como esto:

  PROCEDIMIENTO Intercambia(X,Y: ENTEROS)
  variables
    auxiliar: ENTERO
 
  inicio
 
  auxiliar <- X
  X <- Y
  Y <- auxiliar
 
  fin 

y lo podemos llamar desde cualquier punto del programa principal sin m�s que poner

LLAMAR_A Intercambia(Un_Valor,Y_Otro_Valor)

Sin embargo, como ya he comentado, en C no tenemos procedimientos, �c�mo salvamos este escollo?. Pues lo salvamos con una funci�n como la siguiente:

  void Intercambia(int *x,int *y)  {
   int aux;
   aux=*x; *x=*y; *y=aux;
   } 

y llamamos a esta funci�n desde el programa principal como sigue:

  int main(void) {
   int a=2,b=3;
 
   printf("\nPrimero, a=%d y b=%d\n",a,b);
   Intercambia(&a,&b);
   printf("\npero ahora, a=%d y b=%d\n",a,b);
   return 0;
   } 

Veamos cosas: para empezar, al llamar a la funci�n Intercambia, no le hemos pasado los valores de las variables (paso por valor), sino que le hemos pasado las direcciones de las variables (paso por referencia). As� pues, los par�metros formales de la funci�n Intercambia est�n recibiendo direcciones de memoria, por lo que deben declararse como punteros, cosa que hemos hecho en la cabecera de la funci�n, indicando que recibe dos punteros.

Ahora veamos la funci�n: en primer lugar, creo la variable auxiliar necesaria para hacer el cambio. Sin embargo, en vez de hacer:

aux <- x

hago:

aux <- *x

es decir, asigno a aux el contenido de la direcci�n de memoria a la que apunta x. Como, al llamar la funci�n, hemos pasado unas direcciones de memoria, *x da como resultado el contenido de la direcci�n de memoria donde se guarda la variable a.

Si hubiera hecho:

aux <- x

en aux tendr� almacenada la posici�n de memoria de la variable a y no su contenido, que es lo que yo quiero.

A continuaci�n hacemos:

*x <- *y

es decir, al contenido de la posici�n de memoria a la que apunta x se le asigna el contenido de la posici�n de memoria a la que apunta y.

Y ya termina el intercambio con:

*y <- aux

con lo que al contenido de la posici�n de memoria a la que apunta y se le asigna el valor de la variable aux, que era el contenido de la posici�n de memoria a la que apuntaba x originalmente, en otras palabras, el valor de la variable a.

Si tras leer con calma este punto un par de veces os perdeis, me dejais una nota con las l�neas que os parezcan m�s oscuras, porque esto es un buen trabalenguas mental O:)

La otra gran utilidad es la reserva din�mica de memoria. Si quereis, puedo dar aqu� unos peque�os esbozos, pero eso ya quedar�a para otro cap�tulo. Creo que con 400 l�neas sobre punteros, para empezar, ya teneis bastante O:)

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP
SIGUIENTE ARTÍCULO