wandres.dev
EXPERTO · Punteros II: avanzado

Punteros II: función, dobles y const

Punteros a función para el despacho dinámico, dobles punteros para modificar punteros, y const correctness: el arte de expresar qué puede cambiar y qué no.

⏱ 15 min

Con los fundamentos de punteros dominados, subimos de nivel: punteros que apuntan a funciones (la base del polimorfismo en C), punteros a punteros (para modificar el propio puntero), y la disciplina de const que documenta y protege tus intenciones.

🎯 Al terminar esta lección sabrás
  • Punteros a función y despacho dinámico.
  • Dobles punteros y cuándo hacen falta.
  • const correctness: puntero a const vs puntero const.
  • restrict como promesa de no-solapamiento.

Punteros a función

Una función también vive en memoria, así que puedes tener un puntero a ella y llamarla a través del puntero. Esto habilita el despacho dinámico — elegir qué código correr en tiempo de ejecución:

int sumar(int a, int b) { return a + b; }
int restar(int a, int b) { return a - b; }

int (*op)(int, int);       // puntero a función que toma (int,int) y devuelve int
op = sumar;
printf("%d\n", op(3, 4));  // 7
op = restar;
printf("%d\n", op(3, 4));  // -1
Los punteros a función son el polimorfismo de C

C no tiene clases ni métodos virtuales, pero tiene punteros a función, y con ellos se construye todo lo demás. Una tabla de punteros a función es una vtable hecha a mano: así implementa el kernel de Linux sus drivers (file_operations es una struct llena de punteros a función), así funcionan los callbacks, los plugins y las máquinas de estados. Cuando ves una struct con campos que son funciones, estás viendo orientación a objetos implementada explícitamente. Dominar este patrón te abre la puerta a arquitecturas flexibles en un lenguaje “sin objetos”.

Dobles punteros

Un puntero a puntero (int **) hace falta cuando una función debe modificar el puntero del llamador, no solo el valor apuntado. Recuerda: todo se pasa por valor, incluidos los punteros.

void asignar(int **destino) {
    *destino = malloc(sizeof(int));   // modifica el puntero del llamador
    **destino = 42;
}

int *p = nullptr;
asignar(&p);            // pasamos la dirección del puntero
printf("%d\n", *p);     // 42; ahora p apunta a memoria válida

Sin el doble puntero, asignar solo cambiaría su copia local de p. También aparecen en arrays de strings (char **argv) y estructuras de datos enlazadas.

const correctness

const declara que algo no debe modificarse. Bien usado, documenta tu API y deja que el compilador atrape violaciones. Con punteros hay dos cosas que pueden ser const — el dato o el puntero:

const int *p;        // puntero a const: no puedes cambiar *p, sí p
int *const q = &x;   // puntero const: puedes cambiar *q, no q
const int *const r = &x;  // ambos fijos

Léelo de derecha a izquierda: const int *p = “p es un puntero a un int constante”.

// una función que promete NO modificar lo que recibe:
size_t longitud(const char *s) {   // el const dice "solo leo s"
    size_t n = 0;
    while (s[n]) n++;
    return n;
}
💡
const en los parámetros es un contrato

Marcar const los punteros de entrada que solo lees (const char *, const int *) tiene triple beneficio: documenta que la función no toca tus datos, permite al compilador atrapar modificaciones accidentales, y deja pasar datos que de verdad son constantes. Es gratis y comunica intención. Fíjate en la librería estándar: strlen(const char *), memcmp(const void *, const void *, size_t) — todo lo que se lee sin modificar lleva const. Adopta el hábito.

restrict: la promesa de no solapamiento

restrict (en un parámetro puntero) le promete al compilador que ese puntero es la única vía de acceso a esa memoria en ese ámbito. Le permite optimizar de forma agresiva:

void copiar(int *restrict dst, const int *restrict src, size_t n) {
    for (size_t i = 0; i < n; i++) dst[i] = src[i];
}

Es una promesa tuya: si dst y src se solapan, el resultado es indefinido. Por eso memcpy (que exige no solapamiento) es más rápido que memmove (que lo tolera). Úsalo cuando de verdad garantices el no-solapamiento.

⚔️ Punteros de nivel experto
  1. Declara un array de punteros a función y despacha según un índice (una mini máquina de estados).
  2. Escribe una función que asigne memoria a través de un doble puntero (int **).
  3. Añade const a los parámetros de entrada de tus funciones y comprueba que el compilador atrapa una modificación accidental.
  4. Escribe una copia con restrict y razona por qué habilita optimización.