Una introducción gentil a los punteros usando el lenguaje de programación C

Los punteros son uno de esos conceptos que no tienen sentido cuando los aprendes por primera vez. Usualmente, un cambio de perspectiva es suficiente para que algo en tu cerebro haga click y - ¡puf! - todo mágicamente cae en su lugar.

A pesar de esta confusión inicial, los punteros son herramientas extremadamente poderosas. Además, en algunos lenguajes (como C o Go) entenderlos es imprescindible si quieres ser un desarrollador competente.

Exploremos los punteros usando el lenguaje de programación C y un par de ejemplos muy simples.

El humilde puntero

Los punteros son variables que contienen una referencia a algo en memoria. Sé que suena confuso, pero con un par de ejemplos, tendrá más sentido. Mira el siguiente código y el resultado de ejecutarlo.

#include "stdio.h"
#include "string.h"

void print_character_info(char *char_pointer);

int main() {
  char alphabet[10] = {'a', 'b', 'c', 'd', 'e'};
  char *alphabet_pointer;

  // alphabet_pointer apunta al principio de alphabet, el carácter 'a'
  alphabet_pointer = alphabet;
  print_character_info(alphabet_pointer);

  // Ahora. hagamos que apunte un carácter adelante
  alphabet_pointer++;
  print_character_info(alphabet_pointer);

  // Uno más, ahora debería apuntar a 'c'
  alphabet_pointer++;
  print_character_info(alphabet_pointer);

  //  Uno más, ahora debería apuntar a 'd'
  alphabet_pointer++;
  print_character_info(alphabet_pointer);

  // Movamos el puntero al último elemento del array, 'e'
  alphabet_pointer++;
  print_character_info(alphabet_pointer);

  return 0;
}


void print_character_info(char *char_pointer){
  printf("La dirección del puntero es: %p \n", &char_pointer);
  printf("Apunta a la dirección: %p\n", char_pointer);
  printf("La dirección a la que apunta contiene el valor: %c\n\n", *char_pointer);
}

Cuando se ejecuta, imprime lo siguiente:

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d2e
La dirección a la que apunta contiene el valor: a

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d2f
La dirección a la que apunta contiene el valor: b

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d30
La dirección a la que apunta contiene el valor: c

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d31
La dirección a la que apunta contiene el valor: d

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d32
La dirección a la que apunta contiene el valor: e

Un análisis gentil, paso a paso

Vamos línea por línea para entender mejor qué pasa. Primero, declaramos las siguientes dos variables:

  char alphabet[10] = {'a', 'b', 'c', 'd', 'e'};
  char *alphabet_pointer;
  • alphabet es un array de caracteres, que contiene las primeras 5 letras del alfabeto español.
  • alphabet_pointer es un puntero de tipo char. En C, definimos un puntero anteponiendo el asterisco ‘*’ al nombre de la variable. Es importante declarar el tipo correcto de puntero: tiene que coincidir con el tipo de datos al que apunta, de otra forma, las cosas se pueden poner un poco raras (hablaremos de eso después).

A partir de los resultados de ejecutar el código, podemos formar una imagen del contenido de la memoria inmediatamente después de la declaración de nuestras variables:

alt text

Como puedes ver, el puntero no contiene nada útil al principio.

Apuntando, apuntando, apuntando

En nuestro código, inmediatamente después de declarar las variables, realizamos dos acciones:

  alphabet_pointer = alphabet;
  print_character_info(alphabet_pointer);
  1. Hacemos que alphabet_pointer apunte al principio de alphabet.
  2. Inspeccionamos el estado del puntero usando print_character_info.

Como resultado, obtenemos esta información útil en la consola:

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d2e
La dirección a la que apunta contiene el valor: a

La dirección de alphabet_pointer permanece igual, no realizamos ningún cambio en la posición del puntero mismo. La parte interesante es su contenido, ahora establecido al valor 0x7fff95b03d2e. Revisa la tabla, ¿puedes encontrarlo? ¡Sí, es el principio de alphabet! Precisamente, la dirección que contiene el valor ‘a’.

Puedes acceder al valor contenido en la dirección a la que se apunta usando el operador ‘*’. La siguiente línea imprime el valor ‘a’ si se ejecuta en este momento exacto de la ejecución:

    // *alphabet_pointer devolverá el valor 'a'
    printf("%c", *alphabet_pointer);

Este es el estado actual de la memoria, en forma de imagen bonita:

alt text

Como puedes ver, el contenido de nuestro puntero es una dirección simple. Eso significa que podemos cambiar a qué apunta sobre la marcha. C hace esto muy fácil manejando la aritmética de punteros por nosotros, como estamos a punto de ver.

Aritmética de punteros

La aritmética de punteros significa que puedes realizar operaciones aritméticas básicas en punteros para manipular a qué dirección apuntan. Para lograr esto, puedes sumar y restar enteros relativos a su posición actual. De nuevo, esto es más fácil de entender con un ejemplo.

En nuestro código, realizamos repetidamente las siguientes dos operaciones:

  alphabet_pointer++;
  print_character_info(alphabet_pointer);

La segunda línea es nuestra confiable función print_character_info, nada nuevo. La primera línea, sin embargo, es bastante genial: tomamos alphabet pointer y aumentamos su valor en 1, y como resultado, ahora estamos apuntando a la siguiente dirección en línea (la que contiene ‘b’). Después de realizar la acción una vez, obtenemos los siguientes resultados:

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d2f
La dirección a la que apunta contiene el valor: b

Y después de realizar esa acción una vez más:

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d30
La dirección a la que apunta contiene el valor: c

Continuamos el proceso hasta que eventualmente apunta al último elemento del array: el carácter ‘e’. Al final, la memoria se ve así:

alt text

Los tipos de punteros son importantes

¿Recuerdas cuando dije que el tipo de un puntero es importante? Una de las razones es que el compilador usa el tipo para realizar cálculos de aritmética de punteros.

Diferentes tipos de variables se representan en memoria usando diferentes cantidades de bytes. En mi computadora los Chars se representan usando solo un byte (y solo una ‘ranura de dirección’). Los Ints, por otro lado, necesitan 4 bytes.

Esto significa que cuando escribo pointer++ para un puntero char, me estoy moviendo a la dirección inmediatamente después de la actual. Si realizo el ++ en un puntero int, saltará 4 posiciones adelante. Podemos ver este efecto con un ejemplo de código diferente:

// El resto del código es igual
int main() {
  char alphabet[10] = {'a', 'b', 'c', 'd', 'e'};
  // Nota que ahora alphabet_pointer es de tipo int
  int *alphabet_pointer;

  alphabet_pointer = alphabet;
  print_character_info(alphabet_pointer);

  alphabet_pointer++;
  print_character_info(alphabet_pointer);

  return 0;
}
// El resto del código es igual

Después de imprimir ‘a’ - igual que en nuestro primer ejemplo - hacemos ++ al puntero. Porque ahora es de tipo int, saltará adelante 4 posiciones, apuntando a -lo adivinaste- la dirección con el carácter ‘e’.

Podemos verificar esto fácilmente ejecutando el código, que imprime:

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d2e
La dirección a la que apunta contiene el valor: a

La dirección del puntero es: 0x7fff95b03d08
Apunta a la dirección: 0x7fff95b03d32
La dirección a la que apunta contiene el valor: e

Hay otra razón importante para especificar el tipo del puntero. Como acabas de leer, diferentes variables tienen diferentes tamaños en memoria. Así que cuando necesitamos realizar una desreferenciación para obtener un valor, el compilador necesita saber cuántos bytes debe tomar y cómo interpretar esa data.

Este último tema, junto con el diseño de memoria y el casting de variables/punteros merecen sus propios artículos. Podríamos volver después para hablar de ellos.

También necesito aclarar que los punteros mismos toman varias posiciones de memoria. En las ilustraciones, lo simplifiqué como una sola dirección conteniendo todo el puntero. En realidad, requieren tantos bytes como sea necesario para representar una dirección en tu arquitectura de CPU (típicamente 4 u 8).

Los punteros son geniales, ¿verdad?

Espero que leer este artículo te haya ayudado a entender los punteros, o al menos despierte tu interés en el tema. Podrías requerir algo más de tiempo y experimentación para ganar un entendimiento completo, así que siéntete libre de tomar el ejemplo de código y cambiar un par de cosas. Modifícalo y lee la salida, este enfoque siempre me ha ayudado cuando aprendo nuevos conceptos de programación.

Muchas otras cosas importantes se construyen basadas en punteros. Incluso si trabajas principalmente en lenguajes de programación de alto nivel, entender cómo funcionan las cosas por debajo es útil.

Qué hacer a continuación:

  • Comparte este artículo con amigos y colegas. Gracias por ayudarme a llegar a personas que podrían encontrar útil esta información.
  • El segundo capítulo de Hacking: The Art of Exploitation tiene una introducción increíble a punteros y diseño de memoria. Pruébalo si quieres aprender más sobre este tema.
  • Envíame un email con preguntas, comentarios o sugerencias (está en la página Autor).

Juan Luis Orozco Villalobos

¡Hola! Soy Juan, ingeniero de software y consultor en Budapest. Me especializo en computación en la nube e IA, y me encanta ayudar a otros a aprender sobre tecnología e ingeniería