El código compilado vive en memoria igual que los datos, así que una función tiene una dirección. Un puntero a función es una variable que guarda una, y en cuanto puedes guardar una función puedes pasarla como argumento, mantener una tabla de ellas o elegir entre varias en tiempo de ejecución.
Esa es la capacidad detrás de ordenar con un criterio propio, los manejadores de eventos, las interfaces de plugins y las máquinas de estados. También es la parte de la sintaxis de C que más probablemente hace que un lector se detenga y entrecierre los ojos, así que empezamos por ahí.
La sintaxis de la declaración
Toma una función corriente:
int add(int a, int b);
Para declarar un puntero que pueda guardar su dirección, reemplaza el nombre por (*nombre):
int (*op)(int, int);
Léelo de dentro hacia afuera: op es un puntero, a una función, que toma (int, int) y devuelve int.
Los paréntesis no son decoración. Sin ellos, el * se asocia al tipo de retorno:
int (*f)(int); // puntero a una funcion que toma int y devuelve int
int *g(int); // FUNCION que toma int y devuelve int* - completamente distinto
Aquí está funcionando de punta a punta:
Ambas formas de llamada funcionan porque un designador de función se convierte a puntero automáticamente. Escribe op(10, 3); la grafía (*op)(10, 3) es una reliquia.
Una nota sobre ese último printf: %p espera un puntero a objeto, y los punteros a función son formalmente otra familia, así que imprimir uno no es estrictamente portable. La conversión mantiene callados a los compiladores en las plataformas donde funciona; rara vez necesitarás imprimir uno.
typedef lo vuelve legible
La sintaxis de la declaración se pone fea rápido, y typedef es el remedio estándar:
Compara int apply(BinaryOp op, int x, int y) con la forma cruda, int apply(int (*op)(int, int), int x, int y). El mismo significado; una de las dos se lee de un vistazo. Mira typedef para las reglas generales.
Callbacks: dejar que quien llama aporte el comportamiento
Un callback es una función que le entregas a otra función para que te llame de vuelta en el momento adecuado. Permite que una parte del código se encargue de la estructura de un trabajo mientras quien llama aporta la decisión.
Sin punteros a funciones escribirías count_even, count_positive y count_big: tres copias del mismo bucle. Con ellos, el bucle se escribe una vez y la prueba es el parámetro.
La recompensa de verdad: qsort
El qsort de la biblioteca estándar es el ejemplo de punteros a funciones que de verdad vas a usar. Ordena un array de lo que sea, y lo logra sin saber nada de los elementos salvo su tamaño y un comparador que tú proporcionas.
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
El comparador recibe dos const void * —las direcciones de dos elementos— y devuelve un número negativo si el primero va antes que el segundo, cero si empatan y positivo si va después.
Dos hábitos que llevarse de ese comparador. Nunca escribas return x - y;: parece ingenioso y se desborda con valores grandes o negativos, produciendo un signo equivocado y un array mal ordenado. Convierte antes de desreferenciar, no después: *(const int *)a, porque *a sobre un void * no tiene nada que leer.
La misma maquinaria ordena estructuras por el campo que quieras:
El modismo (q->score > p->score) - (q->score < p->score) devuelve exactamente -1, 0 o 1 sin restas y sin desbordamiento. Y strcmp ya tiene la convención de retorno correcta, así que un comparador de cadenas cabe en una línea.
Tablas de punteros a funciones
Un array de punteros a funciones convierte una cadena de if/else en una búsqueda. Así es como los intérpretes despachan opcodes y como los sistemas de menús ejecutan comandos.
Agregar una operación ahora es agregar una fila, no editar un switch. Combina eso con estructuras y tienes la forma estándar de un plugin en C: una estructura de punteros a funciones es lo que significa "una interfaz" en este lenguaje.
Cosas que muerden
Las firmas deben coincidir exactamente. El tipo de un puntero a función incluye los tipos de sus parámetros y su tipo de retorno. Asignar una función que no coincide es una violación de restricción, y llamar a través de un puntero con el tipo equivocado es comportamiento indefinido incluso cuando "funciona". No conviertas la firma de un comparador para callar un aviso: arregla el comparador.
Un puntero a función nulo sigue siendo un puntero nulo. Inicialízalo a NULL y compruébalo antes de llamar, exactamente igual que con los punteros a datos:
if (handler != NULL) {
handler(event);
}
Mira punteros nulos para entender por qué esa comprobación se gana su lugar.
Los punteros a void pierden la seguridad de tipos, y el sistema de tipos eres tú. Dentro de un comparador de qsort, nada te impide convertir al tipo equivocado. Si ordenas un array de double con compare_ints, compila sin quejas y produce disparates.
La sintaxis de los declaradores se anida. Una función que devuelve un puntero a función es int (*get_op(char c))(int, int);. Cuando te descubras escribiendo eso, recurre a typedef:
typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c); // lo mismo, pero legible
Preguntas frecuentes
¿Cómo se declara un puntero a función en C?
Escribe la firma de la función con (*nombre) donde iría el nombre de la función: int (*op)(int, int); declara op como un puntero a una función que toma dos int y devuelve int. Los paréntesis alrededor de *op son obligatorios: sin ellos declaras una función que devuelve un puntero.
¿Cómo se llama a una función a través de un puntero?
Con op(3, 4) o con (*op)(3, 4): ambas formas son legales y hacen lo mismo, porque un designador de función se convierte a puntero automáticamente. El código C moderno usa la forma simple op(3, 4).
¿Qué es un callback en C?
Una función que le entregas a otra función para que te llame de vuelta. qsort es el ejemplo clásico: le pasas un comparador, y qsort lo llama cada vez que necesita saber cuál de dos elementos va primero. Eso es lo que permite que una sola rutina de ordenamiento funcione con cualquier tipo.
¿Hace falta el & para tomar la dirección de una función?
No. El nombre de una función ya se convierte en un puntero a esa función, así que op = add; y op = &add; son equivalentes. La mayoría del código C omite el &.