Il codice compilato vive in memoria proprio come i dati, quindi una funzione ha un indirizzo. Un puntatore a funzione è una variabile che ne memorizza uno, e una volta che puoi memorizzare una funzione puoi passarla come argomento, tenerne una tabella o scegliere tra più funzioni a runtime.
È la capacità che sta dietro all'ordinamento con un criterio personalizzato, ai gestori di eventi, alle interfacce per i plugin e alle macchine a stati. È anche il pezzo di sintassi C che più facilmente costringe chi legge a fermarsi e strizzare gli occhi, quindi partiamo da lì.
La sintassi della dichiarazione
Prendi una funzione qualsiasi:
int add(int a, int b);
Per dichiarare un puntatore che possa contenerne l'indirizzo, sostituisci il nome con (*name):
int (*op)(int, int);
Leggila dall'interno: op è un puntatore, a una funzione, che prende (int, int), e restituisce int.
Le parentesi non sono decorative. Senza di esse, * si lega al tipo di ritorno:
int (*f)(int); // puntatore a una funzione che prende int e restituisce int
int *g(int); // FUNZIONE che prende int e restituisce int*: tutt'altra cosa
Ecco un esempio completo in funzione:
Entrambe le forme di chiamata funzionano perché il nome di una funzione si converte automaticamente in un puntatore. Scrivi op(10, 3); la forma (*op)(10, 3) è un retaggio del passato.
Una nota sull'ultima printf: %p si aspetta un puntatore a oggetto, e i puntatori a funzione formalmente appartengono a una famiglia separata, quindi stamparne uno non è strettamente portabile. Il cast mette a tacere i compilatori sulle piattaforme dove funziona; ti capiterà raramente di doverne stampare uno.
typedef la rende leggibile
La sintassi della dichiarazione diventa brutta in fretta, e typedef è il rimedio standard:
Confronta int apply(BinaryOp op, int x, int y) con la forma grezza, int apply(int (*op)(int, int), int x, int y). Stesso significato; una delle due si legge a colpo d'occhio. Vedi typedef per le regole generali.
Callback: il chiamante fornisce il comportamento
Una callback è una funzione che passi a un'altra funzione perché questa possa richiamarti al momento giusto. Permette a un pezzo di codice di gestire la struttura di un lavoro mentre il chiamante fornisce la decisione.
Senza puntatori a funzione scriveresti count_even, count_positive e count_big: tre copie dello stesso ciclo. Con i puntatori, il ciclo si scrive una volta e il parametro è il test.
Il vero vantaggio: qsort
La funzione qsort della libreria standard è l'esempio di puntatore a funzione che userai davvero. Ordina un array di qualsiasi cosa, e ci riesce senza sapere nulla degli elementi tranne la loro dimensione e un comparatore che fornisci tu.
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
Il comparatore riceve due const void *, gli indirizzi di due elementi, e restituisce un numero negativo se il primo va prima del secondo, zero se sono pari, positivo se va dopo.
Due abitudini da portarti dietro da quel comparatore. Non scrivere mai return x - y;: sembra furbo ma va in overflow con valori grandi o negativi, producendo un segno sbagliato e un array ordinato male. Fai il cast prima di dereferenziare, non dopo: *(const int *)a, perché *a su un void * non ha niente da leggere.
Lo stesso meccanismo ordina struct in base a qualsiasi campo:
L'idioma (q->score > p->score) - (q->score < p->score) restituisce esattamente -1, 0 o 1 senza sottrazioni e senza overflow. E strcmp ha già la convenzione di ritorno giusta, quindi un comparatore di stringhe sta in una riga.
Tabelle di puntatori a funzione
Un array di puntatori a funzione trasforma una catena di if/else in una ricerca in tabella. È così che gli interpreti smistano gli opcode e che i sistemi a menu eseguono i comandi.
Aggiungere un'operazione ora significa aggiungere una riga, non modificare uno switch. Abbinalo alle struct e ottieni la forma standard di un plugin in C: una struct di puntatori a funzione è ciò che in questo linguaggio si intende per "interfaccia".
Cose che mordono
Le firme devono corrispondere esattamente. Il tipo di un puntatore a funzione include i tipi dei parametri e il tipo di ritorno. Assegnare una funzione con una firma diversa viola un vincolo del linguaggio, e chiamare tramite un puntatore del tipo sbagliato è comportamento indefinito anche quando "funziona". Non fare il cast della firma di un comparatore per zittire un avviso: correggi il comparatore.
Un puntatore a funzione nullo resta un puntatore nullo. Inizializzalo a NULL e controllalo prima di chiamarlo, esattamente come con i puntatori ai dati:
if (handler != NULL) {
handler(event);
}
Vedi puntatori nulli per capire perché quel controllo vale la pena.
I puntatori void perdono la sicurezza dei tipi, e il sistema dei tipi sei tu. Dentro un comparatore di qsort, niente ti impedisce di fare il cast al tipo sbagliato. Se ordini un array di double con compare_ints, compila senza problemi e produce risultati senza senso.
La sintassi dei dichiaratori si annida. Una funzione che restituisce un puntatore a funzione si scrive int (*get_op(char c))(int, int);. Quando ti ritrovi a scrivere una cosa del genere, usa typedef:
typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c); // la stessa cosa, ma leggibile
Domande frequenti
Come si dichiara un puntatore a funzione in C?
Scrivi la firma della funzione con (*nome) al posto del nome della funzione: int (*op)(int, int); dichiara op come puntatore a una funzione che prende due int e restituisce un int. Le parentesi attorno a *op sono obbligatorie: senza di esse dichiari una funzione che restituisce un puntatore.
Come si chiama una funzione tramite un puntatore?
Con op(3, 4) oppure con (*op)(3, 4): sono entrambe valide e fanno la stessa cosa, perché il nome di una funzione si converte automaticamente in un puntatore. Il codice C moderno usa la forma semplice op(3, 4).
Cos'è una callback in C?
Una funzione che passi a un'altra funzione perché questa possa richiamarti. qsort è l'esempio classico: gli passi un comparatore, e qsort lo chiama ogni volta che deve sapere quale di due elementi viene prima. È questo che permette a una sola routine di ordinamento di funzionare con qualsiasi tipo.
Serve la & per prendere l'indirizzo di una funzione?
No. Il nome di una funzione si converte già in un puntatore a quella funzione, quindi op = add; e op = &add; sono equivalenti. La maggior parte del codice C omette la &.