Menu

Les pointeurs de fonction en C : syntaxe, callbacks et qsort

Les fonctions ont aussi des adresses, et un pointeur de fonction en stocke une. Cette page décode la syntaxe de déclaration, montre comment typedef la rend lisible, et construit jusqu'aux callbacks, à qsort avec un comparateur personnalisé et aux tables de dispatch.

Cette page contient des éditeurs exécutables - modifiez, exécutez et voyez la sortie instantanément.

Le code compilé vit en mémoire tout comme les données, donc une fonction a une adresse. Un pointeur de fonction est une variable qui en stocke une - et dès que vous pouvez stocker une fonction, vous pouvez en passer une en argument, en tenir une table, ou choisir entre plusieurs à l'exécution.

C'est la capacité derrière le tri avec un ordre personnalisé, les gestionnaires d'événements, les interfaces de greffons et les machines à états. C'est aussi le morceau de syntaxe C le plus susceptible de faire plisser les yeux d'un lecteur, alors commençons par là.

La syntaxe de déclaration

Prenez une fonction ordinaire :

int add(int a, int b);

Pour déclarer un pointeur capable de contenir son adresse, remplacez le nom par (*nom) :

int (*op)(int, int);

Lisez-le de l'intérieur : op est un pointeur, sur une fonction, prenant (int, int), renvoyant int.

Les parenthèses ne sont pas décoratives. Sans elles, le * s'attache au type de retour :

int (*f)(int);   // pointeur sur une fonction prenant int, renvoyant int
int *g(int);     // FONCTION prenant int, renvoyant int* - completement different

Le voici de bout en bout :

Les deux formes d'appel fonctionnent parce qu'un désignateur de fonction se convertit automatiquement en pointeur. Écrivez op(10, 3) ; l'écriture (*op)(10, 3) est un vestige.

Une note sur ce dernier printf : %p attend un pointeur d'objet, et les pointeurs de fonction forment formellement une famille distincte, donc en afficher un n'est pas strictement portable. Le cast fait taire les compilateurs sur les plateformes où cela marche ; vous aurez rarement besoin d'en afficher un.

typedef rend cela lisible

La syntaxe de déclaration devient vite laide, et typedef est le remède standard :

Comparez int apply(BinaryOp op, int x, int y) avec la forme brute, int apply(int (*op)(int, int), int x, int y). Même sens ; l'une des deux se lit d'un coup d'œil. Voir typedef pour les règles générales.

Les callbacks : laisser l'appelant fournir le comportement

Un callback est une fonction que vous confiez à une autre fonction pour qu'elle puisse vous rappeler au bon moment. Cela permet à un morceau de code de gérer la structure d'un travail pendant que l'appelant fournit la décision.

Sans pointeurs de fonction, vous écririez count_even, count_positive et count_big - trois copies de la même boucle. Avec eux, la boucle est écrite une fois et le test est le paramètre.

Le vrai bénéfice : qsort

Le qsort de la bibliothèque standard est l'exemple de pointeur de fonction que vous utiliserez vraiment. Il trie un tableau de n'importe quoi, et il y parvient en ne sachant rien des éléments sauf leur taille et un comparateur que vous fournissez.

void qsort(void *base, size_t nmemb, size_t size,
           int (*compar)(const void *, const void *));

Le comparateur reçoit deux const void * - les adresses de deux éléments - et renvoie un nombre négatif si le premier se classe avant le second, zéro s'ils sont à égalité, positif s'il se classe après.

Deux habitudes à retenir de ce comparateur. N'écrivez jamais return x - y; - cela paraît malin et déborde pour des valeurs grandes ou négatives, produisant un mauvais signe et un tableau mal trié. Convertissez avant de déréférencer, pas après : *(const int *)a, car *a sur un void * n'a rien à lire.

La même machinerie trie des structures selon le champ que vous voulez :

L'idiome (q->score > p->score) - (q->score < p->score) renvoie exactement -1, 0 ou 1 sans soustraction et sans débordement. Et strcmp a déjà la bonne convention de retour, donc un comparateur de chaînes tient en une ligne.

Les tables de pointeurs de fonction

Un tableau de pointeurs de fonction transforme une chaîne de if/else en une consultation. C'est ainsi que les interpréteurs dispatchent les opcodes et que les systèmes de menu exécutent les commandes.

Ajouter une opération revient désormais à ajouter une ligne, pas à modifier un switch. Associez cela aux structures et vous obtenez la forme standard de greffon en C : une structure de pointeurs de fonction, voilà ce que signifie « une interface » dans ce langage.

Ce qui mord

Les signatures doivent correspondre exactement. Le type d'un pointeur de fonction inclut ses types de paramètres et son type de retour. Affecter une fonction discordante est une violation de contrainte, et appeler à travers un pointeur mal typé est un comportement indéfini même quand « ça marche ». Ne convertissez pas la signature d'un comparateur pour faire taire un avertissement - corrigez le comparateur.

Un pointeur de fonction nul reste un pointeur nul. Initialisez à NULL et vérifiez avant d'appeler, exactement comme pour les pointeurs de données :

if (handler != NULL) {
    handler(event);
}

Voir les pointeurs nuls pour comprendre pourquoi cette vérification vaut son prix.

Les pointeurs void perdent la sûreté de typage, et c'est vous le système de types. Dans un comparateur qsort, rien ne vous empêche de convertir vers le mauvais type. Si vous triez un tableau de double avec compare_ints, cela compile sans broncher et produit n'importe quoi.

La syntaxe des déclarateurs s'imbrique. Une fonction renvoyant un pointeur de fonction s'écrit int (*get_op(char c))(int, int);. Quand vous vous surprenez à écrire cela, prenez un typedef :

typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c);           // la meme chose, lisible

Questions fréquentes

Comment déclare-t-on un pointeur de fonction en C ?

Écrivez la signature de la fonction avec (*nom) là où irait le nom de la fonction : int (*op)(int, int); déclare op comme pointeur sur une fonction prenant deux int et renvoyant un int. Les parenthèses autour de *op sont obligatoires - sans elles, vous déclarez une fonction renvoyant un pointeur.

Comment appelle-t-on une fonction via un pointeur ?

Soit op(3, 4), soit (*op)(3, 4) - les deux sont légaux et font la même chose, car un désignateur de fonction se convertit automatiquement en pointeur. Le code C moderne utilise la forme simple op(3, 4).

Qu'est-ce qu'un callback en C ?

Une fonction que vous confiez à une autre fonction pour qu'elle puisse vous rappeler. qsort en est l'exemple classique : vous passez un comparateur, et qsort l'appelle chaque fois qu'il a besoin de savoir lequel de deux éléments vient en premier. C'est ce qui permet à une seule routine de tri de fonctionner sur n'importe quel type.

Faut-il le & pour prendre l'adresse d'une fonction ?

Non. Un nom de fonction se convertit déjà en pointeur sur cette fonction, donc op = add; et op = &add; sont équivalents. La plupart du code C omet le &.

Coddy programming languages illustration

Apprendre à coder avec Coddy

COMMENCER