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.
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.
- Punteros a función y despacho dinámico.
- Dobles punteros y cuándo hacen falta.
- const correctness: puntero a const vs puntero const.
restrictcomo 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
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;
}
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 sí 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.
- Declara un array de punteros a función y despacha según un índice (una mini máquina de estados).
- Escribe una función que asigne memoria a través de un doble puntero (
int **). - Añade
consta los parámetros de entrada de tus funciones y comprueba que el compilador atrapa una modificación accidental. - Escribe una copia con
restricty razona por qué habilita optimización.