De los punteros en el lenguaje C

24 de noviembre de 2018 • fjbalon

Los punteros son uno de esos elementos más confusos y complejos cuando nos iniciamos en el lenguaje C, sea que vengamos de aprender directamente a programar, o sea que vengamos de otros lenguajes de más alto nivel, como C# o Python; los punteros suelen ser una característica que conlleva sufrimiento y sacrificio. Esto son parte de mis apuntes, para dar algo de claridad a este tema, espero que el lector pueda darle utilidad.

Cada posición de memoria tiene asociada una dirección que la identifica de forma única, algo así como un identificador. Dentro de ellas se almacenan instrucciones y datos. Para que al programador le resulte más sencillo referirse a una dirección de memoria se utilizan las variables, que no son más que nombres asociados a direcciones de memoria concretas.

Las variables de tipo puntero se utilizan para almacenar en ellas la dirección de memoria de otra variable. El puntero, como variable que es, tiene asociado su dirección de memoria; y su contenido será la dirección de memoria de otra variable. Entiendo que esto es muy confuso, quizá con la siguiente imagen esté algo más claro:

Los punteros pueden apuntar a variables previamente declaradas o a variables dinámicas. La potencia de los punteros reside en poder crear (reservar memoria) y destruir (liberar memoria) una variable (dinámica) en tiempo de ejecución. Un puntero ocupa normalmente 4 bytes y se declara siempre de acuerdo con el tipo de dato que apunta. Tenga en cuenta que la declaración de punteros en C se realiza anteponiendo * al nombre de la variable, tal que: int * x;

El lenguaje C dispone de los operadores de dirección (&) e indirección (*), permitiendo respectivamenta acceder a la dirección asociada a una determinada variable y acceder al contenido de ésta (zona de memoria) a la que apunta el puntero.

int i, j, *p;
p = &i;
*p = 10;
p = &j;
*p = -2;
p = NULL;

Voy a tratar de explicar el código anterior:

  1. int i, j, *p; define las variables estáticas i y j, y el puntero p.
  2. p = &i; declara o asigna a la variable p la sección de memoria de la variable i.
  3. *p = 10; asigna a la dirección de memoria a la que apunta p el valor 10.
  4. p = &j; cambia el puntero a la variable j.
  5. *p = -2; asigna dinámicamente a p el valor -2.
  6. p = NULL; hace que el puntero ya no apunte a ninguna.

Podemos trabajar con punteros de tipo indefinido con void *, pudiendo asignarse cualquier tipo de puntero y valor; es un puntero genérico a alguna parte donde se almacena información de algún tipo. Es el programador el responsable de definir estos "algún" eliminando esta ambigüedad.

int x = 1;
float r = 1.0;

void* ptr = &x;
*(int *)ptr = 2;

ptr = &r;
*(float *) ptr = 1.1;

Las operaciones aritméticas entre punteros en el lenguaje C se limitan a suma, resta, comparación y asignación. Estas operaciones a un tipo de dato x tienen automáticamente en cuenta el tamaño que ocupa ese tipo de dato en memoria. Es decir, el número de bytes necesario para almacenar un objetivo de ese tipo de dato: la suma incrementa la dirección a la que apunta el puntero teniendo en cuenta el tipo de dato al que apunte el puntero. O sea, si un puntero apunta a un tipo de dato entero y le suma 1, se desplaza el tamaño de un entero. En el caso de la resta, se decrementa la dirección.

Estas dos operaciones son útiles cuando trabajamos con vectores o matrices (lineal) y la suma o resta permitirían moverse entre los elementos de éste.

int a, b, c;
int *p1, *p2;
void *p;

p1 = &a;
*p1 = 1;

p2 = &b;
*p2 = 2;

p1 = p2;
*p1 = 0;

p2 = &c;
*p2 = 3;

p = &p1;
*p = p2;
*p1 = 1;