CAPÍTULO 1: ¿Qué es un puntero?

Una de las dificultades que suelen encontrar los principiantes de C es el concepto de puntero. El objetivo de este tutorial es dar a los principiantes de C una introducción de los punterros y cómo utilizarlos.

Muchas veces he visto que la razón principal de los problemas que tienen los principiantes con los punteros es la carencia o el mínimo conocimiento de las variables (como las utilizadas en C). Por eso, empezaremos con las variables de C en general.

Una variable en un programa es algo con un nombre cuyo valor puede cambiar. El compilador y el enlazador asignan un bloque concreto de memoria del ordenador para alojar el valor de esta variable. El tamaño de ese bloque depende del rango que tiene esa variable para cambiar su valor. Por ejemplo, en un PC de 32 bit, el tamaño de una variable de tipo entero (integer) es de 4 bytes. En PCs antiguos de 16 bit los enteros tienen 2 bytes. En C, el tamaño de una variable entera no es necesariamente el mismo y puede variar entre máquinas. Además, hay más de un tipo de variable entera en C. Tenemos enteros (int), enteros largos (long) y enteros cortos (short) que puedes leer en cualquier programa básico en C. Este documento asume que utilizamos un sistema de 32 bit con enteros de 4 bytes.

Para conocer el tamaño de los distintos tipos de enteros que soporta tu sistema, ejecuta el siguiente código:

#include <stdio.h>

int main()
{
  printf("capacidad de un short: %d\n", sizeof(short));
  printf("capacidad de un int: %d\n", sizeof(int));
  printf("capacidad de un long: %d\n", sizeof(long));
}

Cuando declaramos una variable, le estamos diciendo al compilador dos cosas: el nombre de la variable y su tipo. Por ejemplo, declaramos una variable de tipo entero con el nombre k así:

    int k; 

La parte "int" de esta declaración indica al compilador que reserve 4 bytes de memoria (en un PC) para alojar el valor del entero. Además, prepara una tabla de símbolos donde agrega el símbolo k y la dirección en memoria donde están esos 4 bytes.

Además, si después escribimos:

    k = 2; 

esperamos que cuando el programa ejecute esta sentencia aloje el valor 2 en el lugar donde k tiene reservado su espacio en memoria. En C llamamos "objeto" a una variable como el entero k.

Hay dos "valores" asociados con el objeto k. Uno es el valor del entero alojado en él (2 del ejemplo anterior) y el otro es el "valor" de la ubicación de la memoria, esto es, la dirección de k. Algunos textos se refieren a estos dos valores con la nomenclatura rvalue (right value, pronunciado "are value") y lvalue (left value, pronunciado "el value") respectivamente.

En algunos lenguajes, lvalue es el valor permitido en el lado izquierdo del operador de asignación '=' (la dirección donde queda el resultado de la evaluación del lado derecho del operador de asignación). rvalue es lo que está en el lado derecho de la instrucción de asignación (el 2 anterior). El rvalue no puede utilizarse en el lado izquierdo de la instrucción de asignación. Por eso 2 = k no está permitido.

En realidad, la definición anterior de "lvalue" está adaptada en C según K&R II (página 197):

"Un objeto es una región de almacenamiento con un nombre; un lvalue es una expresión que se refiere a un objeto."

En cualquier caso hasta ahora, la definición antes citada originalmente es suficiente. A medida que nos familiaricemos con los punteros entraremos en más detalles.

Bien, ahora consideremos lo siguiente:

   int j, k; 

   k = 2; 
   j = 7;    <-- línea 1 
   k = j;    <-- línea 2 

En este código, el compilador interpreta la j de la línea 1 como la dirección de la variable j (su lvalue) y crea el código para copiar el valor 7 a esa dirección. Sin embargo, en la línea 2, j lo interpreta como su rvalue (porque está en el lado derecho del operador de asignación '='). Esto es, j se refiere al valor guardado en la ubicación de memoria de j, que en este caso es 7. Por tanto, el 7 se copia a la dirección asignada por el lvalue de k.

En estos ejemplos utilizamos enteros de 4 bytes, y por tanto cuando se copia un rvalue de un sitio a otro se están copiando 4 bytes. De la misma forma, si los enteros son de 2 bytes, estaremos copiando 2 bytes.

Imagina que por alguna razón necesitamos que una variable aloje un lvalue (una dirección). El tamaño para este valor dependerá del sistema. En ordenadores antiguos de 64k de memoria total, la dirección de cualquier punto de memoria ocupa 2 bytes. Los ordenadores con más memoria necesitarán más bytes para alojar una dirección. El tamaño actual necesario no es demasiado importante pues siempre hay una manera de decirle al compilador que lo que queremos guardar es una dirección.

Esta variable se denomina una variable puntero (por razones que espero se aclaren después). Cuando en C definimos una variable puntero, lo hacemos con un nombre precedido por un asterisco. En C también le asignamos un tipo al puntero que, en este caso, se refiere al tipo del dato guardado en la dirección donde apuntará nuestro puntero. Por ejemplo, consideremos la siguiente declaración de variable:

   int *ptr;

ptr es el nombre de nuestra variable (como k fue el nombre de nuestra variable entera). El '*' indica al compilador que queremos una variable puntero, es decir, reserva los bytes necesarios para almacenar una dirección de memoria. int indica que queremos utilizar nuestra variable puntero para alojar la dirección de un entero. Se dice que este puntero "apunta a" un entero. Sin embargo, hay que tener en cuenta que cuando escribimos int k; no le estamos dando un valor a k. Si esta definición se realiza fuera de cualquier función compatible con ANSI, el compilador la inicializará a cero. Igualmente, ptr no tiene valor porque en la declaración no hemos guardado una dirección en él. En este caso, de nuevo, si la declaración se realiza fuera de cualquier función, se inicializará a un valor que no apuntará a ningún objeto o función de C. Un puntero inicializado de esta manera se llama puntero "null (nulo)".

El patrón actual de bits utilizado para un puntero nulo puede o no evaluarse como cero porque depende específicamente del sistema donde se desarrolle el código. Para que el código fuente sea compatible entre compiladores de varios sistemas se utiliza una macro para representar un puntero nulo. Esta macro se llama NULL. De esta forma, estableciendo el valor del puntero con la macro NULL (ptr = NULL), se garantiza que el puntero es un puntero nulo. De la misma forma que podemos evaluar si el valor de un entero es cero, como en if(k == 0), también podemos evaluar si un puntero es nulo con if (ptr == NULL).

Volvamos a utilizar nuestra nueva variable ptr. Supongamos que queremos guardar en ptr la dirección de nuestra variable entera k. Para hacerlo utilizamos el operador unario & y escribimos:

    ptr = &k; 

Lo que hace el operador & es conseguir el lvalue (la dirección) de k, incluso aunque k esté en el lado derecho del operador de asignación '=', y lo copia al contenido de nuestro puntero ptr. Ahora decimos que ptr "apunta a" k. Ahora sólo queda por ver un operador más.

El "operador de desreferencia" es el asterisco y se utiliza de la siguiente forma:

    *ptr = 7; 

copiará 7 a la dirección donde apunta ptr. De esta forma, si ptr "apunta a" (contiene la dirección de) k, la declaración anterior establecerá el valor de k a 7. Esto es, cuando utilizamos '*' de esta manera nos referimos al valor a donde ptr está apuntando, no al valor en sí del puntero.

Igualmente, podemos escribir:

 printf("%d\n",*ptr); 

para mostrar por pantalla el valor del entero alojado en la dirección donde está apuntando ptr;.

Una forma de ver cómo todo esto encaja sería la de ejecutar el siguiente programa y luego revisar con atención el código y la salida.

------------ Programa 1.1 -------------------------------- 

/* Programa 1.1 de PTRTUT10.TXT   6/10/97 */

#include <stdio.h>

int j, k;
int *ptr;

int main(void)
{
    j = 1;
    k = 2;
    ptr = &k;
    printf("\n");
    printf("j tiene el valor %d y se aloja en %p\n", j, (void *)&j);
    printf("k tiene el valor %d y se aloja en %p\n", k, (void *)&k);
    printf("ptr tiene el valor %p y se aloja en %p\n", ptr, (void *)&ptr);
    printf("El valor del entero apuntado por ptr es %d\n", *ptr);

    return 0;
}

Nota: Aún tenemos que ver los aspectos de C que requieren el uso de la expresión (void *) utilizada aquí. Por el momento, lo utilizaremos en nuestro código de prueba y explicaremos más adelante la razón que hay detrás de esta expresión.


Para repasar:

Referencias del Capítulo 1:

  1. "The C Programming Language" 2nd Edition
    B. Kernighan and D. Ritchie
    Prentice Hall
    ISBN 0-13-110362-8
Continuar con el Tutorial de Punteros

Tabla de Contenidos