Licence CC 0

Les pointeurs de fonction

Dernière mise à jour :

À ce stade, vous pensiez sans doute avoir fait le tour des pointeurs, que ces derniers n’avaient plus de secrets pour vous et que vous maîtrisiez enfin tous leurs aspects ainsi que leur syntaxe parfois déroutante ? Eh bien, pas encore ! :diable:

Il reste un dernier type de pointeur (et non des moindres) que nous avons tu jusqu’ici : les pointeurs de fonction.

Déclaration et initialisation

Jusqu’à maintenant, nous avons manipulé des pointeurs sur objet, c’est-à-dire des adresses vers des zones mémoires contenant des données (des entiers, des flottants, des structures, etc.). Toutefois, il est également possible de référencer des instructions et ceci est réalisé en C à l’aide des pointeurs de fonction.

Un pointeur de fonction se définit à l’aide d’une syntaxe mélangeant celle des pointeurs sur tableau et celles des prototypes de fonction. Sans plus attendre, voici ci-dessous la définition d’un pointeur sur une fonction retournant un int et attendant un int comme argument.

1
int (*pf)(int);

Comme vous le voyez, il est nécessaire, tout comme les pointeurs sur tableau, d’entourer le symbole * et l’identificateur de parenthèses, ici afin d’éviter que cette déclaration ne soit vue comme un prototype et non comme un pointeur de fonction. Autre particularité : le type de retour, le nombre d’arguments et leur type doivent également être spécifiés.

Initialisation

Ok… Et je lui affecte comment l’adresse d’une fonction, moi, à ce machin ? D’ailleurs, elles ont une adresse, les fonctions ? :euh:

Oui et comme d’habitude, cela est réalisé à l’aide de l’opérateur &. ^^
En fait, dans le cas des fonctions, il n’est pas obligatoire de recourir à cet opérateur, ainsi, les deux syntaxes suivantes sont correctes.

1
2
3
4
int (*pf)(int);

pf = &fonction;
pf = fonction;

Ceci est dû, à l’image des tableaux, à une conversion implicite : sauf s’il est l’opérande de l’opérateur &, un identificateur de fonction est converti en un pointeur sur cette fonction. L’utilisation de l’opérateur & est donc facultative, mais elle a le mérite de clarifier un peu les choses. Pour cette raison, nous utiliserons cette syntaxe dans la suite de ce cours.

Utilisation

Déréférencement

Un pointeur de fonction s’emploie de la même manière qu’un pointeur classique, si ce n’est que l’opérateur * et l’identificateur doivent à nouveau être entre parenthèses. Pour le reste, la liste des arguments suit l’expression déréférencée, comme pour un appel de fonction classique.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>


static int triple(int a)
{
    return a * 3;
}


int main(void)
{
    int (*pt)(int) = &triple;

    printf("%d.\n", (*pt)(3));
    return 0;
}
1
9.

Toutefois, particularité des fonctions oblige, sachez que le déréférencement n’est pas nécessaire. Ceci à cause de la conversion implicite expliquée précédemment : un identificateur de fonction est, sauf s’il est l’opérande de l’opérateur &, converti en un pointeur sur cette fonction. L’appel triple(3) cache donc en fait un pointeur de fonction qui, comme vous le voyez, n’est pas déréférencé.

Heu… Mais pourquoi l’expression (*pt)(3) ne provoque-t-elle pas une erreur si c’est un pointeur de fonction qui est nécessaire lors d’un appel ?

Parce que la conversion implicite aura lieu juste après le déréférencement. :-°
Eh oui, déréférencé un pointeur de fonction, c’est un peu reculer pour mieux sauter : nous obtenons une expression équivalente à un identificateur de fonction qui sera ensuite convertie en un pointeur de fonction. Les deux expressions suivantes sont donc équivalentes.

1
2
triple(3);
(*pt)(3);

L’intérêt d’employer le déréférencement est purement syntaxique : cela permet de distinguer des appels effectuer via des pointeurs des appels de fonction classiques.

Passage en argument

Comme n’importe quel pointeur, un pointeur de fonction peut être passé en argument d’une autre fonction (c’est d’ailleurs tout l’intérêt de ceux-ci, comme nous le verrons bientôt). Pour ce faire, il vous suffit d’employer la même syntaxe que pour une déclaration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>


static int triple(int a)
{
    return a * 3;
}


static int quadruple(int a)
{
    return a * 4;
}


static void affiche(int a, int (*pf)(int))
{
    printf("%d.\n", (*pf)(a));
}


int main(void)
{
    affiche(3, &triple);
    affiche(3, &quadruple);
    return 0;
}
1
2
9.
12.

La fonction affiche() ci-dessus est ce que l’on appelle une fonction de rappel (callback function en anglais), c’est-à-dire une fonction faisant appel à une autre à l’aide de l’adresse qui lui est fournie en argument.

Retour de fonction

Dans l’autre sens, il est possible de retourner un pointeur de fonction à l’aide d’une syntaxe… un peu lourde. :-°

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stddef.h>
#include <stdio.h>


static void affiche_pair(int a)
{
    printf("%d est pair.\n", a);
}


static void affiche_impair(int a)
{
    printf("%d est impair.\n", a);
}


static void (*affiche(int a))(int)
{
    if (a % 2 == 0)
        return &affiche_pair;
    else
        return &affiche_impair;
}



int main(void)
{
    void (*pf)(int);
    int a = 2;

    pf = affiche(a);
    (*pf)(a);
    return 0;
}
1
2 est pair.

Comme pour une variable de type pointeur de fonction, le symbole * doit être entouré de parenthèses ainsi que l’identificateur qui le suit. Toutefois, lorsqu’il s’agit du type de retour d’une fonction, la liste des arguments doit également être placée entre ces parenthèses.

Dans cet exemple, la fonction affiche() attend un int et retourne un pointeur sur une fonction ne retournant rien et utilisant un argument de type int. Suivant si a est pair ou impair, la fonction affiche() retourne l’adresse de la fonction affichage_pair() ou affichage_impair() qui est recueillie par le pointeur pf de la fonction main().

Pointeurs de fonction et pointeurs génériques

Vous le savez, le type void est employé en C pour produire un pointeur générique qui peut se voir assigner n’importe quel type de pointeur et être converti vers n’importe quel type de pointeurs. Cette définition est toutefois incomplète car il doit en fait être précisé que cela ne fonctionne que pour des pointeurs sur des objets. Le code ci-dessous est donc incorrect.

1
2
3
4
5
int (*pf)(int);
void *p;

pf = p; /* Faux. */
p = pf; /* Faux également. */

Pareillement, une conversion explicite d’un pointeur sur un objet vers ou depuis un pointeur sur fonction est interdite (ou, plus précisément, son résultat est indéterminé). Ceci exclut donc l’utilisation de l’indicateur p de la fonction printf() pour afficher un pointeur de fonction.

1
printf("%p.\n", (void *)pf); /* Faux. */

Toutefois, les pointeurs de fonction disposent de leur propre pointeur « générique ». Nous utilisons ici les guillemets car il ne l’est pas tout à fait puisqu’il peut notamment être utilisé à l’inverse d’un pointeur sur void. Un pointeur « générique » de fonction se déclare comme un pointeur de fonction, mais en ne spécifiant que le type de retour.

1
int (*pf)();

Un tel pointeur peut se voir assigner n’importe quel pointeur sur fonction du moment que le type de retour de celui-ci est identique au sien. Inversement, ce pointeur « générique » peut être affecté à un autre pointeur sous la même condition. Dans notre cas, le type de retour doit donc être int.

1
2
3
4
5
6
7
8
9
int (*f)(int, int);
int (*g)(char, char, double);
void (*h)(void);
int (*pf)();

pf = f; /* Ok. */
pf = g; /* Ok. */
pf = h; /* Faux car le type de retour de `h` est `void`. */
f = pf; /* Ok. */

Il existe cependant une exception supplémentaire : une fonction à nombre variables d’arguments ne peut pas être affectée à un tel pointeur, même si le type de retour est identique. Nous verrons bientôt de quoi il s’agit, mais pour l’heure, sachez que les fonctions de la famille de printf() et de scanf() sont concernées par cette règle.

La promotion des arguments

là, minute papillon ! Il se passe quoi si j’utilise le pointeur pf avec une fonction qui attend normalement des arguments ?

Excellente question ! :)

Vous vous en doutez : les arguments peuvent toujours être envoyé à la fonction référencée. Cependant, il y a une subtilité. Étant donné qu’un pointeur « générique » de fonction ne fournit aucune information quant aux arguments, le compilateur ne peut pas convertir ceux-ci vers le type attendu par la fonction. Ainsi, si vous fournissez un int et que la fonction attend un char, le int ne sera pas converti vers le type char par le compilateur.

Toutefois, dans un tel cas, plusieurs conversions implicites sont appliquées afin de limiter les types possibles (on parle de « promotion des arguments ») :

  1. Un argument de type entier de rang inférieur ou égal à celui du type int (soit char et short le plus souvent) est converti vers le type int (ou unsigned int si le type int ne peut pas représenter toutes les valeurs du type d’origine).
  2. Un argument de type float et converti vers le type double.

Ceci signifie qu’une fonction appelée à l’aide d’un pointeur « générique » de fonction ne pourra jamais recevoir des arguments de type char, short ou float.

Illustration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>


static float triple(float f)
{
    return 3.F * f;
}


static short quadruple(short n)
{
    return 4 * n;
}


int main(void)
{
    float (*pt)() = &triple;
    short (*pq)() = &quadruple;

    printf("triple = %f.\n", (*pt)(3.F)); /* Faux. */
    printf("quadruple = %d.\n", (*pq)(2)); /* Faux. */
    return 0;
}

Les pointeurs nuls

L’absence de conversions par le compilateur dans le cas où aucune information n’est fournie par rapport aux arguments pose un problème particulier dans le cas des pointeurs nuls et tout spécialement lors de l’usage de la macroconstante NULL.

Rappelez-vous : un pointeur nul est construit en convertissant, soit explicitement, soit implicitement, zéro (entier) vers un type pointeur. Or, étant donné que le compilateur n’effectuera aucune conversion implicite dans notre cas, nous ne pouvons compter que sur les conversions explicites.

Et c’est ici que le bât blesse : la macroconstante NULL a deux valeurs possibles : (void *)0 ou 0, le choix étant laissé aux différents systèmes. La première ne pose pas de problème, mais la seconde en pose un plutôt gênant : c’est un int qui sera passé comme argument et non un pointeur nul.

Dès lors, lorsque vous employez un pointeur « générique » de fonction, vous devez recourir à une conversion explicite si vous souhaitez produire un pointeur nul.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
static void affiche(char *chaine)
{
    if (chaine != NULL)
        puts(chaine);
}

/* ... */

void (*pf)() = &affiche;

(*pf)(NULL); /* Faux. */
(*pf)(0); /* Faux. */
(*pf)((char*)0); /* Ok. */

En résumé
  1. Un pointeur de fonction permet de stocker une référence vers une fonction ;
  2. Il n’est pas nécessaire d’employer l’opérateur & pour obtenir l’adresse d’une fonction ;
  3. Le déréférencement n’est pas obligatoire lors de l’utilisation d’un pointeur de fonction ;
  4. Une fonction employant un pointeur vers une autre fonction reçu en argument est appelée une fonction de rappel (callback function en anglais) ;
  5. Le recours à une structure permet d’éviter les problèmes de définitions récursives ;
  6. Il est possible d’utiliser un pointeur « générique » de fonction en ne fournissant aucune information quant aux arguments lors de sa définition ;
  7. Un pointeur « générique » de fonction peut être converti vers ou depuis n’importe quel autre type de pointeur de fonction du moment que le type de retour reste identique ;
  8. Lors de l’utilisation d’un pointeur « générique » de fonction, les arguments transmis sont promus, mais aucune conversion implicite n’est réalisée par le compilateur ;
  9. Lors de l’utilisation d’un pointeur « générique » de fonction, un pointeur nul ne peut être fourni qu’à l’aide d’une conversion explicite.