PUNTEROS





Introducción .
El uso de punteros en C supone, por su proximidad a las direcciones del lenguaje máquina, una fuente de potencia y flexibilidad. Pero precisamente por su proximidad al lenguaje máquina los punteros suelen ser difíciles de entender en un primer momento, y ciertamente son la fuente de numerosos errores. Por tanto, conviene meditar (no hay otra expresión) sobre los conceptos asociados a estas estructuras del lenguaje.

Un puntero es una variable que contiene la dirección de una posición de memoria. Esa posición de memoria es la dirección de comienzo de una variable; la variable puede ser dinámica .

Esta es la esencia del concepto: una dirección de memoria, esto es, la dirección de un determinado byte. Esto puede suponer una cierta dificultad conceptual cuando encontramos declaraciones de puntero como las siguientes:
int * p;
float * q;
double * r;
¿Qué tiene que ver un int , o un float , o un double , con un puntero? Todas las direcciones de memoria son iguales, luego puede parecer sobrante asociar un tipo al puntero p . Sin embargo, tiene sentido. Dada una dirección de memoria, ¿cómo se pude saber la extensión de la variable que comienza en ella? ¿Cómo saber el tipo de esa variable? Verdaderamente, es imprescindible asociar un tipo a los punteros: sólo así podremos interpretar correctamente la variable cuya dirección contienen.

Declaración de punteros .
En general, dado un Tipo atómico, los punteros de ese tipo se declaran en la forma
Tipo * puntero_de_tipo;
esto es: basta intercalar un asterisco entre el nombre del tipo y el nombre de la variable para hacer que la variable declarada sea un puntero de algun objeto del tipo Tipo .

Punteros no iniciados .
En el momento de su declaración, el puntero contiene un valor que no está definido, y no es otro que la trama de bits que haya en memoria en la zona reservada para el puntero por el compilador. Esta trama es ciertamente aleatoria e imprevisible, y puede muy bien ser la dirección de una zona de memoria perteneciente a la aplicación, o más probablemente se tratará de una zona de memoria no perteneciente a aplicación. Si interpretamos la (¡inexistente¡) variable señalada por el puntero, el valor obtenido es impredecible, y lo más probable es que su codificación no se ajuste al tipo de variable asociado al puntero. Si intentamos escribir y la "variable" señalada no reside en el espacio de memoria de nuestra aplicación, el programa se detendrá de inmediato, por haber intentado acceder a una posición que no le pertenece. Si la "variable" señalada reside en la zona de memoria de nuestra aplicación, podremos ciertamente escribir... y muy posiblemente destruiremos información de una variable realmente existente. Esta destrucción de información se notará tarde o temprano, sin que sea posible determinar fácilmente por qué se ha producido. Consiguientemente:

Antes de emplear un puntero para obtener el valor de una variable o para dar valor a la variable señalada por el puntero, es imprescindible almacenar en el puntero la dirección de la zona de memoria deseada.

¿Cómo se le da valor a un puntero? Mediante una operación de asignación. Para obtener la dirección de la zona de memoria que debe señalar el puntero se puede emplear el operador ampersand (&) o bien cualquier función que proporcione un puntero de tipo compatible con el nuestro. En este sentido, hay que tener en cuenta que C aplica a los punteros las mismas reglas de comprobación estricta de tipos que se aplican a todas las demás variables.

La constante NULL .
Como medida preventiva, el lenguaje C ofrece una constante que denota el puntero nulo, esto es, una dirección tal que todo intento de acceder a la variable que comienza en esta dirección (tanto para leer como para escribir) se detecta y supone la parada inmediata del programa. Esta constante se denomina NULL . Es buena práctica de programación asignar el valor NULL a los punteros recién declarados. De esta manera, el intento de utilizar esos punteros sin darles antes un valor válido será detectado y se producirá un error. Esto se haría mediante una declaración de la forma
Tipo * puntero = NULL;


Operador & . El operador & proporciona la dirección de su operando, esto es, del objeto que se le proporciona como argumento. En general, su aplicación tendrá la forma siguiente:
Tipo variable;
Tipo * puntero;
puntero = &variable;
Obsérvese la necesidad de respetar los tipos en las asignaciones de punteros, ya mencionada. Sin embargo, internamente, todas las direcciones ocupan cuatro bytes. Por tanto es posible refundir punteros de distintos tipos, y los resultados son correctos si el usuario conoce realmente la estructura señalada. Véase el Ejercicio 9 .

Operador * y acceso a variables señaladas por punteros
El operador *, antepuesto a una variable de tipo puntero, denota el objeto señalado por ese puntero. A este proceso se denomina "deshacer una indirección". De este modo se consigue una expresión totalmente equivalente al nombre de una variable, que podrá empleare exactamente igual que el nombre de la variable, si existiera. La utilización del operador * será en general de la forma
Tipo variable, valor;
Tipo * puntero;
puntero = &variable;
*puntero = valor; /* Equivale a variable = valor; */
La importancia de poder utilizar indistintamente *puntero y variable se hará patente en el tema dedicado a variables dinámicas . En pocas palabras, existe un mecanismo que permite crear y destruir variables bajo el control directo del programador. Las variables así creadas no poseen nombre, y el acceso a las mismas se efectúa a través de punteros, empleando el operador * para acceder directamente a ellas si son atómicas, o bien otros operadores ( [] , -> ) sin son estructuradas.

Alias .
Las expresiones variable y *puntero de la sección anterior son equivalentes. Se dice entonces que p es un alias de variable. Nada impide crear múltiples alias de una variable, que podrían utilizarse desde distintos lugares de un programa:
float valor;
float *alias_1, *alias_2, *alias_3;
alias_1 = alias_2 = alias_3 = &valor;
El uso de alias es potencialmente peligroso, pues permite modificar el valor de una variable empleando cualquiera de esos alias.

Punteros de tipos estructurados. Baste decir que todos los tipos de variables de C poseen el correspondiente tipo de puntero. De hecho, ¡incluso las funciones tienen sus punteros! En particular, las variables estructurada disponen también de sus tipos de punteros, lo cual hace posible, como se mencionaba más arriba, crear variables estructuradas dinámicamente, para después acceder a ellas de forma análoga al sus equivalentes estáticos o automáticos. El objetivo del uso de punteros en C es, por tanto, doble: El uso de punteros en C puede resultar complejo en un principio, pero hace que el lenguaje resulte ideal para su destino inicial, que era y es la programación de sistemas.

Ejemplo.- Construir un programa que muestre el uso de punteros.
/*
	Este programa muestra el uso elemental de punteros,
	alias etc. Se limita a tipos no estructurados.
*/
#include <stdio.h>
#include <stdlib.h>
#include <time.h>


int main(void)
{
	float un_numero;
	float * puntero_float;
	printf("Punteros elementales.\n\n");
	printf("Antes de la iniciación, puntero_float vale      : %u\n", (unsigned int)puntero_float);
	puntero_float = &un_numero;
	printf("Después de la iniciación, puntero_float vale    : %u\n", (unsigned int)puntero_float);
	printf("Antes de la iniciación, un_numero vale          : %f\n", un_numero);
	/* Damos valor a un_numero a través de un alias */
	*puntero_float = 33.33;
	printf("Después de la iniciación, un_numero vale        : %f\n", un_numero);
	puntero_float = NULL;
	printf("Damos a puntero_float el valor NULL. Ahora vale : %u\n",  (unsigned int)puntero_float);
	printf("Pero el valor de un_numero sigue siendo         : %f\n", un_numero);

	printf("\n\nTerminación normal del programa.\n");
	return 0;
}
/*
	RESULTADO
Punteros elementales.

Antes de la iniciación, puntero_float vale      : 0
Después de la iniciación, puntero_float vale    : 97297868
Antes de la iniciación, un_numero vale          : 0.000000
Después de la iniciación, un_numero vale        : 33.330002
Damos a puntero_float el valor NULL. Ahora vale : 0
Pero el valor de un_numero sigue siendo         : 33.330002


Terminación normal del programa.
*/


Concepto y aplicaciones de la Doble Indirección.
Un puntero es una variable como las demás de C. Ahora bien, toda variable posee su dirección, luego los punteros también tienen dirección (la suya, no la que contienen). Esta dirección se puede almacenar en variables que son igualmente punteros, y cuya declaración es como puede verse en el ejemplo siguiente:
Tipo_base variable;
Tipo_base * ptb = &variable;
Tipo_base ** pptb = &ptb;

El doble asterisco indica que pptb es un "doble puntero" o más exactamente el puntero de un puntero. Entonces se tienen los resultados esperables al aplicar el operador * , que deshace (una) indirección:
*pptb equivale a ptb
**pptb equivale a variable

Estas equivalencias son sumamente importantes: dado el puntero de un puntero, se puede modificar el puntero, del mismo modo que dado el puntero de una variable se puede modificar la variable. Además, como se recordará, todo puntero (y los dobles punteros son punteros, no hay que olvidarlo) se puede complementar con un índice, y el puntero en cuestión pasa a comportarse como si fuese una lista monodimensional, formada por elementos del tipo base. Esto va a tener dos consecuencias importantes, que serán de aplicación en muchos programas:
  1. En la sección dedicada a funciones se estudian los conceptos de paso por valor y paso por referencia. En C, todos los parámetros reales sin excepción pasan por valor. En particular, todos los parámetros que sean punteros pasarán por valor (aunque las variables que señalan pasen por referencia). Esto plantea un problema: si se pasa un puntero como parámetro, el valor del parámetro real pasa por valor y por tanto no es modificable desde el interior de la función. Esto puede ser grave para funciones que reciban un puntero de valor NULL y deseen reservar memoria dinámica internamente, devolviendo a través del puntero recibido la dirección del bloque reservado. Ahora bien, si se pasa un doble puntero como parámetro real, entonces el doble puntero pasa por valor... pero el puntero que señala pasa por referencia! Esto abre la puerta para modificar punteros desde el interior de una función; basta pasarlos como doble puntero (esto es, basta pasar la dirección del puntero que se quiere modificar). Un puntero es una variable como otra cualquiera, y si se quiere que pase a una función por referencia, debe pasarse a esa función la dirección del puntero (que es, decimos, un doble puntero).
  2. La segunda consecuencia importante es la derivada de aplicar un índice a un doble puntero. Como quiera que un doble puntero tiene como tipo base a un puntero normal, el doble puntero se comporta como si fuera el puntero de una lista de punteros normales. Entonces se tiene una situación como esta:
    
    	int * ptb;
    	int ** pptb = &ptb;
    	*pptb[7] = 33;
    	/* Y también... */
    	pptb[7][8] = 1234;
    
    


    El problema ya conocido persiste y se agrava. En primer lugar, se admite aplicar un índice a pptb , anteponer un asterisco y asignar un valor al int designado por esta expresión. En segundo lugar, como pptb[7] es un puntero de int (porque un puntero cualificado con un índice se comporta como una lista de elementos de su tipo base, y el tipo base de pptb es un puntero de int ), se puede cualificar a su vez con otro índice, pues pptb[7] , como puntero de int que es, se comporta como nombre de una hipotética lista de enteros cuyo primer elemento se encuentra en la dirección almaceanda en pptb[7] . ¿Cuál es el problema? El problema es que pptb no ha recibido valor alguno, y apunta a una posición imprevisible de memoria. Los resultados de avanzar respecto a esa posición (con el índice 7), tomar entonces el contenido de esa séptima posición como puntero de int y avanzar entonces 8 posiciones de tipo int , para luego almacenar allí el valor 1234, son decididamente aterradores. Y no hay comprobaciones... pptb puede muy bien contener la dirección de un puntero de int válido, y ese puntero de int puede contener la dirección de una única variable. Con todo, si se hacen las cosas bien (si nos preocupamos de construir una lista de punteros que señalen listas de int), un único doble puntero permite recorrer una lista de punteros de listas de (distintas - verdad?) longitudes.


Ejercicios propuestos



  1. Ejercicio 0701r01.- ¿Cuánto ocupan los punteros de los principales tipos atómicos? Escribir un programa para comprobarlo.

  2. Ejercicio 0701r02.- ¿Es verdad que las variables consecutivas ocupan posiciones consecutivas? Comprobar este hecho para variables atómicas, tanto estáticas (globales) como automáticas (declaradas en una función). ¿Se observan diferencias? ¿Tiene algo qu ever con la pila del programa?

  3. Ejercicio 0701r03.- Construir un programa que muestre la trama de bits de un puntero.

  4. Ejercicio 0701r04.- Construir un puntero de char . Hacer que apunte a una variable estática de tipo char . Imprimir el valor del puntero (%p). Sumar 1 al puntero y mostrar en pantalla el valor del nuevo puntero.

  5. Ejercicio 0701r05.- Construir un puntero de int . Hacer que apunte a una variable estática de tipo int . Imprimir el valor del puntero ( %p ). Sumar 1 al puntero y mostrar en pantalla el valor del nuevo puntero.

  6. Ejercicio 0701r06.- onstruir un puntero de double . Hacer que apunte a una variable estática de tipo double . Imprimir el valor del puntero ( %p ). Sumar 1 al puntero y mostrar en pantalla el valor del nuevo puntero. Comparar los resultados de este ejercicio con los obtenidos en los dos anteriores. ¿Qué significa esto respecto a la aritmética de punteros?

  7. Ejercicio 0701r07.- Construir una lista de enteros. Mostrar las direcciones de todos los elementos de la lista, junto con sus índice. ¿Se puede recorrer una lista con un puntero?

  8. Ejercicio 0701r08.- Construir una tabla de enteros. Mostrar las direcciones de todos los elementos de la tabla junto con sus índices. ¿Se puede recorrer una tabla con un puntero? ¿Y un tarugo? ¿Y cualquier otra matriz de N dimensiones? ¿Cuál es el puntero del último elemento de float a[DIM_1][DIM_2]...[DIM_N] ?

  9. Ejercicio 0701r09.- Considérense las declaraciones siguientes:
    
    struct Registro {
    	char nombre[40];
    	long telefono;
    	float talla;
    };
    struct Nodo {
    	char nombre[40];
    	long telefono;
    	float talla;
    	struct Nodo * sig;
    };
    
    
    Se dispone de una lista enlazada formada por elementos de tipo Nodo , y de una función que admite elementos de tipo Registro con objeto de almacenarlos en disco. Se pide construir un iterador capaz de volcar la lista en disco, haciendo uso de la función mencionada.

  10. Ejercicio 0701r10.- Se desea construir un programa que ilustre la situación que se tiene al emplear punteros y dobles punteros. Para ello, declárese un puntero de int y un puntero de puntero del anterior, en la forma:
    
    int valor;
    int * pint = &entero;
    int ** ppint = &pint;
    
    
    Se pide construir un programa que muestre las direcciones de valor, pint y ppint, y que asigne distintos valores a entero empleando pint y ppint. ¿Cuánto vale pint[0]? ¿Cuánto vale pint[8]? ¿Cuánto vale ppint[0][0]?