Comment fonctionnent les pointeurs de fonction en C?


1235

J'ai eu récemment de l'expérience avec les pointeurs de fonction en C.

Donc, poursuivant la tradition de répondre à vos propres questions, j'ai décidé de faire un petit résumé des bases, pour ceux qui ont besoin d'une plongée rapide sur le sujet.


35
Aussi: pour une analyse approfondie des pointeurs C, voir blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . De plus, la programmation à partir du sol montre comment ils fonctionnent au niveau de la machine. Comprendre le «modèle de mémoire» de C est très utile pour comprendre le fonctionnement des pointeurs C.
Abbafei

8
Grande info. Par le titre cependant, je m'attendais à voir vraiment une explication du fonctionnement des "pointeurs de fonction", pas de la façon dont ils sont codés :)
Bogdan Alexandru

Réponses:


1478

Pointeurs de fonction en C

Commençons par une fonction de base sur laquelle nous allons pointer :

int addInt(int n, int m) {
    return n+m;
}

Tout d'abord, définissons un pointeur sur une fonction qui reçoit 2 ints et renvoie un int:

int (*functionPtr)(int,int);

Maintenant, nous pouvons pointer en toute sécurité vers notre fonction:

functionPtr = &addInt;

Maintenant que nous avons un pointeur sur la fonction, utilisons-le:

int sum = (*functionPtr)(2, 3); // sum == 5

Passer le pointeur à une autre fonction est fondamentalement le même:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

Nous pouvons également utiliser des pointeurs de fonction dans les valeurs de retour (essayez de suivre, cela devient compliqué):

// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
    printf("Got parameter %d", n);
    int (*functionPtr)(int,int) = &addInt;
    return functionPtr;
}

Mais c'est beaucoup plus agréable d'utiliser un typedef:

typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef

myFuncDef functionFactory(int n) {
    printf("Got parameter %d", n);
    myFuncDef functionPtr = &addInt;
    return functionPtr;
}

19
Merci pour la grande info. Pourriez-vous ajouter un aperçu de l'endroit où les pointeurs de fonction sont utilisés ou s'avèrent particulièrement utiles?
Rich.Carpenter

326
"functionPtr = & addInt;" peut également être écrit (et l'est souvent) sous la forme "functionPtr = addInt;" ce qui est également valable puisque la norme dit qu'un nom de fonction dans ce contexte est converti en l'adresse de la fonction.
hlovdal

22
hlovdal, dans ce contexte, il est intéressant d'expliquer que c'est ce qui permet d'écrire functionPtr = ****************** addInt;
Johannes Schaub - litb

105
@ Rich.Carpenter Je sais que c'est 4 ans trop tard, mais je pense que d'autres personnes pourraient en bénéficier: les pointeurs de fonction sont utiles pour passer des fonctions en tant que paramètres à d'autres fonctions . Il m'a fallu beaucoup de recherches pour trouver cette réponse pour une raison étrange. Donc, fondamentalement, cela donne à la pseudo fonctionnalité C de première classe.
giant91

22
@ Rich.Carpenter: les pointeurs de fonction sont agréables pour la détection de CPU d'exécution. Avoir plusieurs versions de certaines fonctions pour tirer parti de SSE, popcnt, AVX, etc. Au démarrage, définissez vos pointeurs de fonction sur la meilleure version de chaque fonction pour le processeur actuel. Dans votre autre code, appelez simplement via le pointeur de fonction au lieu d'avoir des branches conditionnelles sur les fonctionnalités du processeur partout. Ensuite, vous pouvez faire une logique compliquée pour bien décider, même si ce processeur prend en charge pshufb, c'est lent, donc l'implémentation antérieure est toujours plus rapide. x264 / x265 l'utilisent largement et sont open source.
Peter Cordes

304

Les pointeurs de fonction en C peuvent être utilisés pour effectuer une programmation orientée objet en C.

Par exemple, les lignes suivantes sont écrites en C:

String s1 = newString();
s1->set(s1, "hello");

Oui, le ->et le manque d'un newopérateur est un abandon mort, mais cela semble impliquer que nous définissons le texte d'une Stringclasse "hello".

En utilisant des pointeurs de fonction, il est possible d'imiter les méthodes en C .

Comment est-ce accompli?

La Stringclasse est en fait un structavec un tas de pointeurs de fonction qui agissent comme un moyen de simuler des méthodes. Ce qui suit est une déclaration partielle de la Stringclasse:

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

Comme on peut le voir, les méthodes de la Stringclasse sont en fait des pointeurs de fonction vers la fonction déclarée. Lors de la préparation de l'instance de String, la newStringfonction est appelée afin de configurer les pointeurs de fonction vers leurs fonctions respectives:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

Par exemple, la getStringfonction appelée en appelant la getméthode est définie comme suit:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

Une chose qui peut être remarquée, c'est qu'il n'y a pas de concept d'instance d'un objet et d'avoir des méthodes qui sont en fait une partie d'un objet, donc un "self object" doit être transmis à chaque invocation. (Et internalc'est juste un caché structqui a été omis de la liste de code plus tôt - c'est un moyen de masquer des informations, mais cela n'est pas pertinent pour les pointeurs de fonction.)

Donc, plutôt que de pouvoir le faire s1->set("hello");, il faut passer l'objet pour effectuer l'action s1->set(s1, "hello").

Avec cette explication mineure avoir à passer dans une référence à vous - même de la route, nous allons passer à la partie suivante, qui est l' héritage en C .

Disons que nous voulons créer une sous-classe de String, disons an ImmutableString. Afin de rendre la chaîne immuable, la setméthode ne sera pas accessible, tout en maintenant l'accès à getet length, et en forçant le "constructeur" à accepter a char*:

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

Fondamentalement, pour toutes les sous-classes, les méthodes disponibles sont à nouveau des pointeurs de fonction. Cette fois, la déclaration de la setméthode n'est pas présente, par conséquent, elle ne peut pas être appelée dans a ImmutableString.

Quant à l'implémentation de la ImmutableString, le seul code pertinent est la fonction "constructeur", la newImmutableString:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

En instanciant le ImmutableString, la fonction pointe vers les méthodes getet lengthfait référence à la méthode String.getet String.length, en parcourant la basevariable qui est un Stringobjet stocké en interne .

L'utilisation d'un pointeur de fonction peut permettre l'héritage d'une méthode d'une superclasse.

Nous pouvons encore continuer à polymorphisme C .

Si par exemple nous voulions changer le comportement de la lengthméthode pour retourner 0tout le temps dans la ImmutableStringclasse pour une raison quelconque, tout ce qu'il faudrait faire est de:

  1. Ajoutez une fonction qui va servir de lengthméthode prioritaire .
  2. Accédez au "constructeur" et définissez le pointeur de fonction sur la lengthméthode prioritaire .

L'ajout d'une lengthméthode prioritaire dans ImmutableStringpeut être effectué en ajoutant un lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Ensuite, le pointeur de fonction pour la lengthméthode dans le constructeur est connecté au lengthOverrideMethod:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

Maintenant, plutôt que d'avoir un comportement identique pour la lengthméthode en ImmutableStringclasse comme la Stringclasse, maintenant la lengthméthode se référera au comportement défini dans la lengthOverrideMethodfonction.

Je dois ajouter une clause de non-responsabilité que j'apprends toujours à écrire avec un style de programmation orienté objet en C, donc il y a probablement des points que je n'ai pas bien expliqués, ou qui peuvent simplement être hors de propos en termes de meilleure façon d'implémenter la POO en C. Mais mon but était d'essayer d'illustrer l'une des nombreuses utilisations des pointeurs de fonction.

Pour plus d'informations sur la façon d'effectuer une programmation orientée objet en C, veuillez vous référer aux questions suivantes:


22
Cette réponse est horrible! Non seulement cela implique que OO dépend en quelque sorte de la notation par points, mais cela encourage également la mise de déchets dans vos objets!
Alexei Averchenko

27
C'est OO bien, mais pas n'importe où près de l'OO de style C. Ce que vous avez implémenté de manière discontinue est un OO basé sur un prototype de style Javascript. Pour obtenir un OO de style C ++ / Pascal, vous devez: 1. Avoir une structure const pour une table virtuelle de chaque classe avec des membres virtuels. 2. Ayez un pointeur sur cette structure dans les objets polymorphes. 3. Appelez les méthodes virtuelles via la table virtuelle et toutes les autres méthodes directement - généralement en respectant une ClassName_methodNameconvention de dénomination des fonctions. Ce n'est qu'alors que vous obtenez les mêmes coûts d'exécution et de stockage qu'en C ++ et Pascal.
Rétablir Monica

19
Travailler OO avec un langage qui n'est pas destiné à être OO est toujours une mauvaise idée. Si vous voulez OO et que vous avez toujours C, il suffit de travailler avec C ++.
rbaleksandar

20
@rbaleksandar Dites cela aux développeurs du noyau Linux. "toujours une mauvaise idée" est strictement votre avis, avec lequel je suis fermement en désaccord.
Jonathon Reinhart

6
J'aime cette réponse mais ne lancez pas malloc
cat

227

Le guide pour se faire virer: Comment abuser des pointeurs de fonction dans GCC sur les machines x86 en compilant votre code à la main:

Ces littéraux de chaîne sont des octets de code machine x86 32 bits. 0xC3est une retinstruction x86 .

Normalement, vous ne les écrivez pas à la main, vous écrivez en langage assembleur, puis vous utilisez un assembleur comme nasmpour l'assembler dans un binaire plat que vous hexdumpez dans un littéral de chaîne C.

  1. Renvoie la valeur actuelle dans le registre EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Écrire une fonction de swap

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
    
  3. Ecrire un compteur de boucles à 1000, appelant une fonction à chaque fois

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
  4. Vous pouvez même écrire une fonction récursive qui compte jusqu'à 100

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);
    

Notez que les compilateurs placent des littéraux de chaîne dans la .rodatasection (ou .rdatasous Windows), qui est liée en tant que partie du segment de texte (avec le code des fonctions).

Le segment de texte a lu + l' autorisation Exec, donc coulée littéraux de chaîne pour des pointeurs de fonction fonctionne sans avoir besoin mprotect()ou VirtualProtect()appels système comme vous auriez besoin pour la mémoire allouée dynamiquement. (Ou gcc -z execstackrelie le programme avec pile + segment de données + exécutable de tas, comme un hack rapide.)


Pour les désassembler, vous pouvez les compiler pour mettre une étiquette sur les octets et utiliser un désassembleur.

// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";

En compilant gcc -c -m32 foo.cet en désassemblant avec objdump -D -rwC -Mintel, nous pouvons obtenir l'assembly et découvrir que ce code viole l'ABI en encombrant EBX (un registre préservé par les appels) et est généralement inefficace.

00000000 <swap>:
   0:   8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]   # load int *a arg from the stack
   4:   8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]   # ebx = b
   8:   8b 00                   mov    eax,DWORD PTR [eax]       # dereference: eax = *a
   a:   8b 1b                   mov    ebx,DWORD PTR [ebx]
   c:   31 c3                   xor    ebx,eax                # pointless xor-swap
   e:   31 d8                   xor    eax,ebx                # instead of just storing with opposite registers
  10:   31 c3                   xor    ebx,eax
  12:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]  # reload a from the stack
  16:   89 01                   mov    DWORD PTR [ecx],eax     # store to *a
  18:   8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
  1c:   89 19                   mov    DWORD PTR [ecx],ebx
  1e:   c3                      ret    

  not shown: the later bytes are ASCII text documentation
  they're not executed by the CPU because the ret instruction sends execution back to the caller

Ce code machine fonctionnera (probablement) en code 32 bits sous Windows, Linux, OS X, et ainsi de suite: les conventions d'appel par défaut sur tous ces OS passent des arguments sur la pile au lieu d'être plus efficacement dans les registres. Mais EBX est préservé dans toutes les conventions d'appel normales, donc l'utiliser comme registre de travail sans l'enregistrer / le restaurer peut facilement faire planter l'appelant.


8
Remarque: cela ne fonctionne pas si la prévention de l'exécution des données est activée (par exemple sur Windows XP SP2 +), car les chaînes C ne sont normalement pas marquées comme exécutables.
SecurityMatt

5
Salut Matt! Selon le niveau d'optimisation, GCC insérera souvent des constantes de chaîne dans le segment TEXT, donc cela fonctionnera même sur une version plus récente de Windows à condition de ne pas interdire ce type d'optimisation. (IIRC, la version MINGW au moment de mon article il y a plus de deux ans intègre les littéraux de chaîne au niveau d'optimisation par défaut)
Lee

10
quelqu'un pourrait-il expliquer ce qui se passe ici? Quels sont ces littéraux à cordes étranges?
2014

56
@ajay On dirait qu'il écrit des valeurs hexadécimales brutes (par exemple, '\ x00' est identique à '/ 0', ils sont tous les deux égaux à 0) dans une chaîne, puis transtypez la chaîne dans un pointeur de fonction C, puis exécutez le pointeur de la fonction C parce qu'il est le diable.
ejk314

3
salut FUZxxl, je pense que cela peut varier en fonction du compilateur et de la version du système d'exploitation. Le code ci-dessus semble fonctionner correctement sur codepad.org; codepad.org/FMSDQ3ME
Lee

115

Une de mes utilisations préférées pour les pointeurs de fonction est comme itérateurs bon marché et faciles -

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}

7
Vous devez également passer un pointeur vers des données spécifiées par l'utilisateur si vous souhaitez extraire d'une manière ou d'une autre n'importe quelle sortie d'itérations (pensez aux fermetures).
Alexei Averchenko

1
D'accord. Tous mes itérateurs ressembler à ceci: int (*cb)(void *arg, ...). La valeur de retour de l'itérateur me permet également de m'arrêter tôt (si différent de zéro).
Jonathon Reinhart

24

Les pointeurs de fonction deviennent faciles à déclarer une fois que vous disposez des déclarants de base:

  • id: ID: ID est
  • Pointeur: *D: pointeur D
  • Fonction: D(<parameters>): fonction D prise <paramètres de >retour

Alors que D est un autre déclarant construit en utilisant ces mêmes règles. En fin de compte, quelque part, il se termine par ID(voir ci-dessous pour un exemple), qui est le nom de l'entité déclarée. Essayons de construire une fonction prenant un pointeur vers une fonction ne prenant rien et retournant int, et renvoyant un pointeur vers une fonction prenant un char et retournant int. Avec les définitions de type, c'est comme ça

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

Comme vous le voyez, il est assez facile de le construire à l'aide de typedefs. Sans typedefs, ce n'est pas difficile non plus avec les règles de déclaration ci-dessus, appliquées de manière cohérente. Comme vous le voyez, j'ai raté la partie vers laquelle pointe le pointeur et la chose que la fonction retourne. C'est ce qui apparaît à l'extrême gauche de la déclaration, et ce n'est pas intéressant: c'est ajouté à la fin si on a déjà construit le déclarant. Faisons cela. Construire de manière cohérente, premier mot - montrant la structure en utilisant [et ]:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]

Comme vous le voyez, on peut décrire un type complètement en ajoutant des déclarants les uns après les autres. La construction peut se faire de deux manières. La première est ascendante, en commençant par la bonne chose (les feuilles) et en progressant jusqu'à l'identifiant. L'autre façon est de haut en bas, en commençant par l'identifiant, en descendant jusqu'aux feuilles. Je vais montrer les deux sens.

De bas en haut

La construction commence par la chose à droite: la chose retournée, qui est la fonction qui prend le caractère. Pour garder les déclarateurs distincts, je vais les numéroter:

D1(char);

Inséré directement le paramètre char, car il est trivial. Ajout d'un pointeur sur le déclarant en le remplaçant D1par *D2. Notez que nous devons entourer les parenthèses *D2. Cela peut être connu en recherchant la priorité de l' *-operatoropérateur d'appel de fonction (). Sans nos parenthèses, le compilateur le lirait *(D2(char p)). Mais ce ne serait plus un simple remplacement de D1 par *D2, bien sûr. Les parenthèses sont toujours autorisées autour des déclarants. Donc, vous ne faites rien de mal si vous en ajoutez trop, en fait.

(*D2)(char);

Le type de retour est terminé! Maintenant, remplaçons D2par la fonction déclarant la fonction prenant <parameters>return , qui est D3(<parameters>)ce que nous sommes maintenant.

(*D3(<parameters>))(char)

Notez qu'aucune parenthèse n'est nécessaire, car nous voulons D3 être un déclarant de fonction et non un déclarant de pointeur cette fois. Génial, il ne reste que les paramètres pour cela. Le paramètre est fait exactement de la même manière que nous avons fait le type de retour, juste avec charremplacé par void. Je vais donc le copier:

(*D3(   (*ID1)(void)))(char)

J'ai remplacé D2par ID1, car nous avons terminé avec ce paramètre (c'est déjà un pointeur vers une fonction - pas besoin d'un autre déclarant). ID1sera le nom du paramètre. Maintenant, j'ai dit ci-dessus à la fin, on ajoute le type que tous ces déclarants modifient - celui qui apparaît à l'extrême gauche de chaque déclaration. Pour les fonctions, cela devient le type de retour. Pour les pointeurs du type pointé etc ... C'est intéressant quand on écrit le type, il apparaîtra dans l'ordre inverse, tout à droite :) Quoi qu'il en soit, le remplacer donne la déclaration complète. Les deux fois intbien sûr.

int (*ID0(int (*ID1)(void)))(char)

J'ai appelé l'identifiant de la fonction ID0dans cet exemple.

De haut en bas

Cela commence à l'identifiant tout à gauche dans la description du type, enveloppant ce déclarant lorsque nous nous dirigeons vers la droite. Commencez avec la fonction de prise de <paramètres >renvoyant

ID0(<parameters>)

La prochaine chose dans la description (après "retour") était le pointeur sur . Incorporons-le:

*ID0(<parameters>)

Alors la chose suivante était functon prendre des <paramètres de >retour . Le paramètre est un simple caractère, nous le mettons donc à nouveau immédiatement, car il est vraiment trivial.

(*ID0(<parameters>))(char)

Notez les parenthèses que nous avons ajoutées, car nous voulons à nouveau que les *liaisons soient d'abord, puis les (char). Dans le cas contraire , on lirait la fonction prise <paramètres de >retour fonction ... . Non, les fonctions renvoyant des fonctions ne sont même pas autorisées.

Maintenant, nous avons juste besoin de mettre des <paramètres >. Je vais montrer une version courte de la dérivation, car je pense que vous avez déjà l'idée de le faire.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Mettez juste intdevant les déclarants comme nous l'avons fait de bas en haut, et nous avons terminé

int (*ID0(int (*ID1)(void)))(char)

La belle chose

La méthode ascendante ou descendante est-elle meilleure? J'ai l'habitude du bas vers le haut, mais certaines personnes peuvent être plus à l'aise avec le haut vers le bas. C'est une question de goût je pense. Par ailleurs, si vous appliquez tous les opérateurs dans cette déclaration, vous finirez par obtenir un int:

int v = (*ID0(some_function_pointer))(some_char);

C'est une belle propriété de déclarations en C: La déclaration affirme que si ces opérateurs sont utilisés dans une expression utilisant l'identifiant, alors elle donne le type tout à gauche. C'est comme ça pour les tableaux aussi.

J'espère que vous avez aimé ce petit tutoriel! Maintenant, nous pouvons établir un lien vers cela lorsque les gens s'interrogent sur l'étrange syntaxe de déclaration des fonctions. J'ai essayé de mettre le moins de C internes possible. N'hésitez pas à éditer / corriger des choses dedans.


24

Une autre bonne utilisation pour les pointeurs de fonction:
Basculer entre les versions sans douleur

Ils sont très pratiques à utiliser lorsque vous souhaitez différentes fonctions à différents moments ou différentes phases de développement. Par exemple, je développe une application sur un ordinateur hôte doté d'une console, mais la version finale du logiciel sera placée sur un Avnet ZedBoard (qui possède des ports pour les écrans et les consoles, mais ils ne sont pas nécessaires / recherchés pour le version finale). Ainsi, pendant le développement, je vais utiliser printfpour afficher l'état et les messages d'erreur, mais quand j'ai terminé, je ne veux rien imprimer. Voici ce que j'ai fait:

version.h

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

Dans version.cJe définirai la fonction 2 prototypes présents dansversion.h

version.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

Remarquez comment le pointeur de fonction est prototypé en version.htant que

void (* zprintf)(const char *, ...);

Lorsqu'il est référencé dans l'application, il commencera à s'exécuter là où il pointe, ce qui n'a pas encore été défini.

Dans version.c, notez dans la board_init()fonction à laquelle zprintfest affectée une fonction unique (dont la signature de fonction correspond) en fonction de la version définie dansversion.h

zprintf = &printf; zprintf appelle printf à des fins de débogage

ou

zprintf = &noprint; zprintf retourne simplement et n'exécutera pas de code inutile

L'exécution du code ressemblera à ceci:

mainProg.c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

Le code ci-dessus utilisera printfs'il est en mode débogage, ou ne fera rien s'il est en mode release. C'est beaucoup plus facile que de parcourir l'intégralité du projet et de commenter ou de supprimer du code. Tout ce que je dois faire est de changer la version version.het le code fera le reste!


4
Vous risquez de perdre beaucoup de temps de performance. Au lieu de cela, vous pouvez utiliser une macro qui active et désactive une section de code basée sur Debug / Release.
AlphaGoku

19

Le pointeur de fonction est généralement défini par typedefet utilisé comme paramètre et valeur de retour.

Les réponses ci-dessus ont déjà beaucoup expliqué, je donne juste un exemple complet:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}

14

L'une des grandes utilisations des pointeurs de fonction en C est d'appeler une fonction sélectionnée au moment de l'exécution. Par exemple, la bibliothèque d'exécution C a deux routines qsortet bsearchqui prennent un pointeur vers une fonction qui est appelée pour comparer deux éléments en cours de tri; cela vous permet de trier ou de rechercher, respectivement, n'importe quoi, en fonction des critères que vous souhaitez utiliser.

Un exemple très basique, s'il y a une fonction appelée print(int x, int y)qui à son tour peut nécessiter d'appeler une fonction (soit add()ou sub(), qui sont du même type) alors ce que nous ferons, nous ajouterons un argument de pointeur de fonction à la print()fonction comme indiqué ci-dessous :

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

La sortie est:

la valeur est: 410 la
valeur est: 390


10

La fonction de démarrage à partir de zéro a une adresse mémoire à partir de l'endroit où ils commencent à s'exécuter. Dans le langage d'assemblage, ils sont appelés comme (appelez "l'adresse mémoire de la fonction"). Revenez maintenant à C Si la fonction a une adresse mémoire, ils peuvent être manipulés par des pointeurs en C. Donc, selon les règles de C

1.Tout d'abord, vous devez déclarer un pointeur sur la fonction 2.Passez l'adresse de la fonction souhaitée

**** Remarque-> les fonctions doivent être du même type ****

Ce programme simple illustrera chaque chose.

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

entrez la description de l'image iciAprès cela, voyons comment la machine les comprend.Glimpse des instructions machine du programme ci-dessus dans une architecture 32 bits.

La zone de marque rouge montre comment l'adresse est échangée et stockée dans eax. Ensuite, leur est une instruction d'appel sur eax. eax contient l'adresse souhaitée de la fonction.


8

Un pointeur de fonction est une variable qui contient l'adresse d'une fonction. Comme il s'agit d'une variable de pointeur mais avec des propriétés restreintes, vous pouvez l'utiliser à peu près comme vous le feriez pour toute autre variable de pointeur dans les structures de données.

La seule exception à laquelle je peux penser est de traiter le pointeur de fonction comme pointant vers autre chose qu'une seule valeur. Faire de l'arithmétique de pointeur en incrémentant ou décrémentant un pointeur de fonction ou en ajoutant / soustrayant un décalage à un pointeur de fonction n'a pas vraiment d'utilité car un pointeur de fonction ne pointe que sur une seule chose, le point d'entrée d'une fonction.

La taille d'une variable de pointeur de fonction, le nombre d'octets occupés par la variable, peuvent varier en fonction de l'architecture sous-jacente, par exemple x32 ou x64 ou autre.

La déclaration d'une variable de pointeur de fonction doit spécifier le même type d'informations qu'une déclaration de fonction pour que le compilateur C effectue les types de vérifications qu'il effectue normalement. Si vous ne spécifiez pas de liste de paramètres dans la déclaration / définition du pointeur de fonction, le compilateur C ne pourra pas vérifier l'utilisation des paramètres. Il y a des cas où ce manque de contrôle peut être utile, mais n'oubliez pas qu'un filet de sécurité a été supprimé.

Quelques exemples:

int func (int a, char *pStr);    // declares a function

int (*pFunc)(int a, char *pStr);  // declares or defines a function pointer

int (*pFunc2) ();                 // declares or defines a function pointer, no parameter list specified.

int (*pFunc3) (void);             // declares or defines a function pointer, no arguments.

Les deux premières déclarations sont quelque peu similaires en ce sens:

  • funcest une fonction qui prend un intet un char *et renvoie unint
  • pFuncest un pointeur de fonction auquel est affectée l'adresse d'une fonction qui prend un intet un char *et renvoie unint

Donc, à partir de ce qui précède, nous pourrions avoir une ligne source dans laquelle l'adresse de la fonction func()est affectée à la variable de pointeur de fonction pFunccomme dans pFunc = func;.

Notez la syntaxe utilisée avec une déclaration / définition de pointeur de fonction dans laquelle des parenthèses sont utilisées pour surmonter les règles de priorité des opérateurs naturels.

int *pfunc(int a, char *pStr);    // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr);  // declares a function pointer that returns an int

Plusieurs exemples d'utilisation différents

Quelques exemples d'utilisation d'un pointeur de fonction:

int (*pFunc) (int a, char *pStr);    // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr);    // declare a pointer to a function pointer variable
struct {                             // declare a struct that contains a function pointer
    int x22;
    int (*pFunc)(int a, char *pStr);
} thing = {0, func};                 // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr));  // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr));  // declare a function pointer that points to a function that has a function pointer as an argument

Vous pouvez utiliser des listes de paramètres de longueur variable dans la définition d'un pointeur de fonction.

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

Ou vous ne pouvez pas du tout spécifier une liste de paramètres. Cela peut être utile, mais il élimine la possibilité pour le compilateur C d'effectuer des vérifications sur la liste d'arguments fournie.

int  sum ();      // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int  sum2(void);  // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);

Moulages de style C

Vous pouvez utiliser des modèles de style C avec des pointeurs de fonction. Cependant, sachez qu'un compilateur C peut être laxiste sur les vérifications ou fournir des avertissements plutôt que des erreurs.

int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum;               // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum;   // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum;     // compiler error of bad cast generated, parenthesis are required.

Comparer le pointeur de fonction à l'égalité

Vous pouvez vérifier qu'un pointeur de fonction est égal à une adresse de fonction particulière à l'aide d'une ifinstruction, mais je ne sais pas à quel point cela serait utile. D'autres opérateurs de comparaison semblent avoir encore moins d'utilité.

static int func1(int a, int b) {
    return a + b;
}

static int func2(int a, int b, char *c) {
    return c[0] + a + b;
}

static int func3(int a, int b, char *x) {
    return a + b;
}

static char *func4(int a, int b, char *c, int (*p)())
{
    if (p == func1) {
        p(a, b);
    }
    else if (p == func2) {
        p(a, b, c);      // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
    } else if (p == func3) {
        p(a, b, c);
    }
    return c;
}

Un tableau de pointeurs de fonction

Et si vous voulez avoir un tableau de pointeurs de fonction pour chacun des éléments dont la liste d'arguments a des différences, vous pouvez définir un pointeur de fonction avec la liste d'arguments non spécifiée (pas voidqui ne signifie pas d'arguments mais simplement non spécifiée) quelque chose comme le suivant bien que vous peut voir des avertissements du compilateur C. Cela fonctionne également pour un paramètre de pointeur de fonction vers une fonction:

int(*p[])() = {       // an array of function pointers
    func1, func2, func3
};
int(**pp)();          // a pointer to a function pointer


p[0](a, b);
p[1](a, b, 0);
p[2](a, b);      // oops, left off the last argument but it compiles anyway.

func4(a, b, 0, func1);
func4(a, b, 0, func2);  // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);

    // iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
    func4(a, b, 0, p[i]);
}
    // iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
    (*pp)(a, b, 0);          // pointer to a function pointer so must dereference it.
    func4(a, b, 0, *pp);     // pointer to a function pointer so must dereference it.
}

Style C namespaceUtilisation de Global structavec des pointeurs de fonction

Vous pouvez utiliser le staticmot - clé pour spécifier une fonction dont le nom est la portée du fichier, puis l'attribuer à une variable globale afin de fournir quelque chose de similaire à la namespacefonctionnalité de C ++.

Dans un fichier d'en-tête, définissez une structure qui sera notre espace de noms ainsi qu'une variable globale qui l'utilisera.

typedef struct {
   int (*func1) (int a, int b);             // pointer to function that returns an int
   char *(*func2) (int a, int b, char *c);  // pointer to function that returns a pointer
} FuncThings;

extern const FuncThings FuncThingsGlobal;

Puis dans le fichier source C:

#include "header.h"

// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
    return a + b;
}

static char *func2 (int a, int b, char *c)
{
    c[0] = a % 100; c[1] = b % 50;
    return c;
}

const FuncThings FuncThingsGlobal = {func1, func2};

Cela serait ensuite utilisé en spécifiant le nom complet de la variable de structure globale et le nom du membre pour accéder à la fonction. Le constmodificateur est utilisé sur le global pour qu'il ne puisse pas être modifié par accident.

int abcd = FuncThingsGlobal.func1 (a, b);

Domaines d'application des pointeurs de fonction

Un composant de bibliothèque DLL pourrait faire quelque chose de similaire à l' namespaceapproche de style C dans laquelle une interface de bibliothèque particulière est demandée à partir d'une méthode d'usine dans une interface de bibliothèque qui prend en charge la création d'une structfonction contenant des pointeurs. Cette interface de bibliothèque charge la version DLL demandée, crée une structure avec les pointeurs de fonction nécessaires, puis renvoie la structure à l'appelant demandeur pour utilisation.

typedef struct {
    HMODULE  hModule;
    int (*Func1)();
    int (*Func2)();
    int(*Func3)(int a, int b);
} LibraryFuncStruct;

int  LoadLibraryFunc LPCTSTR  dllFileName, LibraryFuncStruct *pStruct)
{
    int  retStatus = 0;   // default is an error detected

    pStruct->hModule = LoadLibrary (dllFileName);
    if (pStruct->hModule) {
        pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
        pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
        pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
        retStatus = 1;
    }

    return retStatus;
}

void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
    if (pStruct->hModule) FreeLibrary (pStruct->hModule);
    pStruct->hModule = 0;
}

et cela pourrait être utilisé comme dans:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

La même approche peut être utilisée pour définir une couche matérielle abstraite pour le code qui utilise un modèle particulier du matériel sous-jacent. Les pointeurs de fonction sont remplis de fonctions spécifiques au matériel par une usine pour fournir la fonctionnalité spécifique au matériel qui implémente les fonctions spécifiées dans le modèle matériel abstrait. Cela peut être utilisé pour fournir une couche matérielle abstraite utilisée par un logiciel qui appelle une fonction d'usine afin d'obtenir l'interface de fonction matérielle spécifique, puis utilise les pointeurs de fonction fournis pour effectuer des actions pour le matériel sous-jacent sans avoir besoin de connaître les détails d'implémentation de la cible spécifique .

Pointeurs de fonction pour créer des délégués, des gestionnaires et des rappels

Vous pouvez utiliser des pointeurs de fonction pour déléguer une tâche ou une fonctionnalité. L'exemple classique en C est le pointeur de fonction délégué de comparaison utilisé avec les fonctions de bibliothèque C standard qsort()et bsearch()pour fournir l'ordre de classement pour trier une liste d'éléments ou effectuer une recherche binaire sur une liste d'éléments triée. Le délégué de la fonction de comparaison spécifie l'algorithme de classement utilisé dans le tri ou la recherche binaire.

Une autre utilisation est similaire à l'application d'un algorithme à un conteneur de bibliothèque de modèles standard C ++.

void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for ( ; pList < pListEnd; pList += sizeItem) {
        p (pList);
    }

    return pArray;
}

int pIncrement(int *pI) {
    (*pI)++;

    return 1;
}

void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for (; pList < pListEnd; pList += sizeItem) {
        p(pList, pResult);
    }

    return pArray;
}

int pSummation(int *pI, int *pSum) {
    (*pSum) += *pI;

    return 1;
}

// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;

ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);

Un autre exemple concerne le code source de l'interface graphique dans lequel un gestionnaire pour un événement particulier est enregistré en fournissant un pointeur de fonction qui est réellement appelé lorsque l'événement se produit. Le cadre Microsoft MFC avec ses mappages de messages utilise quelque chose de similaire pour gérer les messages Windows qui sont remis à une fenêtre ou un thread.

Les fonctions asynchrones qui nécessitent un rappel sont similaires à un gestionnaire d'événements. L'utilisateur de la fonction asynchrone appelle la fonction asynchrone pour démarrer une action et fournit un pointeur de fonction que la fonction asynchrone appellera une fois l'action terminée. Dans ce cas, l'événement est la fonction asynchrone qui termine sa tâche.


0

Étant donné que les pointeurs de fonction sont souvent des rappels typés, vous souhaiterez peut-être consulter les rappels sécurisés de type . Il en va de même pour les points d'entrée, etc. des fonctions qui ne sont pas des rappels.

C est assez volage et pardonne en même temps :)

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.