Un puntero a función es una variable que almacena la dirección de memoria donde comienza el código ejecutable de una función. Mientras que un puntero convencional apunta a datos en el stack, heap o segmento de datos, el puntero a función apunta al segmento de texto (text segment) del binario. Esto permite que el flujo de ejecución de un programa sea dinámico: en lugar de llamar a una función específica por su nombre (vinculación en tiempo de compilación), llamamos a una dirección que puede cambiar durante la ejecución (vinculación en tiempo de ejecución).
La sintaxis de declaración es, posiblemente, una de las más enrevesadas de C. Para declarar un puntero a una función que recibe un int y devuelve un double, se escribe double (*fp)(int). Es vital que los paréntesis rodeen al asterisco (*fp); de lo contrario, el compilador interpretará que fp es una función que devuelve un puntero a double (double *fp(int)), lo cual es algo totalmente distinto.
Este mecanismo es el motor detrás de los callbacks (pasar una función como argumento para que otra la invoque), las tablas de despacho (arrays de punteros a funciones que eliminan la necesidad de switch gigantes) y el polimorfismo en C (estructuras que contienen punteros a funciones para simular métodos de objetos).
Si intentas llamar a un puntero que es NULL o que apunta a una dirección de memoria que no contiene código ejecutable, el sistema operativo lanzará una señal de error, generalmente un SIGSEGV (segmentación). Más peligroso aún es el comportamiento indefinido que ocurre cuando fuerzas un punteador a una función con una firma incompatible (por ejemplo, un puntero que espera dos argumentos cuando la función real solo recibe uno); esto corromperá el estado de la pila (stack) y hará que el programa falle de forma errática o vulnerable.
#include <stdio.h>
/* Definimos un tipo para representar una operación matemática.
Usar typedef es esencial en producción para evitar la "sopa de sintaxis"
y mejorar la legibilidad de las firmas de funciones. */
typedef int (*Operacion)(int, int);
/* Funciones candidatas para ser usadas como punteros a función */
int sumar(int a, int b) { return a + b; }
int restar(int a, int b) { return a - b; }
int multiplicar(int a, int b) { return a * b; }
/* Una estructura que simula un "objeto" con un método.
Este es el patrón base para implementar polimorfismo en C. */
typedef struct {
const char *nombre;
Operacion ejecutar;
} Procesador;
/* Esta es una función que acepta un callback.
No le importa qué hace la función, solo que cumpla la firma. */
void ejecutar_calculadora(int x, int y, Operacion op) {
if (op == NULL) {
fprintf(stderr, "Error: Callback es NULL\n");
return;
}
int resultado = op(x, y);
printf("Resultado de la operacion: %d\n", resultado);
}
int main(void) {
int val1 = 20;
int val2 = 10;
/* 1. Uso directo de un puntero a función */
Operacion fp = sumar;
printf("Suma directa: %d\n", fp(val1, val2));
/* 2. Uso de la función como Callback */
printf("Llamada mediante callback: ");
ejecutar_calculadora(val1, val2, multiplicar);
/* 3. Tabla de despacho (Dispatch Table)
Un array de punteros a función permite evitar múltiples 'if' o 'switch'. */
Operacion tabla_ops[] = { sumar, restar, multiplicar };
const char *nombres[] = { "Suma", "Resta", "Multiplicacion" };
printf("\nIterando tabla de despacho:\n");
for (int i = 0; i < 3; i++) {
// Acceso al puntero en el array y llamada indirecta
printf("%s: %d\n", nombres[i], tabla_ops[i](val1, val2));
}
/* 4. Simulación de polimorfismo con un 'objeto' */
Procesador proc_resta = { .nombre = "Restador Pro", .ejecutar = restar };
printf("\nObjeto %s: %d\n", proc_resta.nombre, proc_resta.ejecutar(val1, val2));
return 0;
}
Desglose del código
En el ejemplo, hemos definido Operacion usando typedef como un tipo int (*)(int, int). Esto simplifica drásticamente la lectura en el resto del código.
En la función ejecutar_calculadora, el parámetro op es un puntero a función. Cuando hacemos op(x, y), el compilador genera una instrucción de salto indirecto (call en x86_64), pasando los argumentos en los registros correspondientes según la ABI (Application Binary Interface) del sistema. La comprobación if (op == NULL) es una práctica de seguridad crítica en sistemas embebidos o de misión crítica para evitar fallos de segmentación.
La tabla_ops es un array de punteros a función. En lugar de usar un switch(tipo) para decidir qué operación hacer, accedemos directamente al índice. Esto es mucho más eficiente en términos de tiempo de ejecución, ya que la complejidad pasa de ser $O(n)$ (en un if-else largo) a $O(1)$ (un simple salto a una dirección calculada).
Finalmente, la estructura Procesador encapsula un dato (nombre) y un comportamiento (ejecutar). Al asignar restar a proc_resta.ejecutar, estamos vinculando una implementación específica a una estructura, permitiendo que diferentes instancias de Procesador se comporten de manera distinta ante la misma llamada, que es la esencia del polimorfismo.
El error frecuente
El error más sutil y peligroso ocurre cuando se fuerza un cast de un puntero a función a una firma incompatible. Aunque el estándar C permite convertir punteros entre tipos de función, el resultado es comportamiento indefinido si la firma no coincide exactamente en los argumentos o el tipo de retorno.
#include <stdio.h>
int funcion_real(int a) {
return a * 2;
}
int main(void) {
// ERROR: Definimos un puntero con firma incorrecta (espera dos ints)
int (*fp_incorrecto)(int, int) = (int (*)(int, int))funcion_real;
// Esto es Comportamiento Indefinido (UB).
// En la práctica, esto puede causar que la CPU busque un segundo
// argumento en un registro que no contiene lo que esperamos,
// o que la pila quede desalineada tras el retorno.
printf("Resultado erróneo: %d\n", fp_incorrecto(10, 20));
return 0;
}
Este tipo de errores son extremadamente difíciles de detectar con herramientas de depuración convencionales como gdb, ya que el programa podría “funcionar” por pura suerte debido a cómo la ABI maneja los registros, pero fallará de forma catastrófica en otra arquitectura o con optimizaciones del compilador. Para detectarlo, es imperativo usar AddressSanitizer o mantener un rigor estricto con las advertencias del compilador (-Wextra -Wpedantic).
N° 48