1465 votes

Comment fonctionnent les pointeurs de fonction en C ?

J'ai eu une certaine expérience dernièrement avec les pointeurs de fonction en C.

Fidèle à la tradition qui consiste à répondre à vos propres questions, j'ai décidé de faire un petit résumé de l'essentiel, pour ceux qui ont besoin d'une plongée rapide dans le sujet.

40 votes

Aussi : Pour une analyse un peu plus approfondie des pointeurs en C, voir blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Aussi, La programmation depuis la base 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 en C.

9 votes

Excellente information. D'après le titre, je m'attendais à voir une explication du fonctionnement des "pointeurs de fonction", et non de leur codage :)

2 votes

La réponse suivante est plus courte et beaucoup plus facile à comprendre : stackoverflow.com/a/142809/2188550

1702voto

Yuval Adam Points 59423

Les pointeurs de fonction en C

Commençons par une fonction de base qui sera pointant vers :

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

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

int (*functionPtr)(int,int);

Nous pouvons maintenant pointer vers notre fonction en toute sécurité :

functionPtr = &addInt;

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

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

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

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 il est beaucoup plus agréable d'utiliser une 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;
}

371 votes

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

26 votes

Hlovdal, dans ce contexte il est intéressant d'expliquer que c'est ce qui permet d'écrire functionPtr = ******************addInt ;

122 votes

@Rich.Carpenter Je sais que c'est 4 ans trop tard, mais je me suis dit que d'autres personnes pourraient en profiter : Les pointeurs de fonction sont utiles pour passer des fonctions comme paramètres à d'autres fonctions. . Il m'a fallu beaucoup de recherches pour trouver cette réponse, pour une raison étrange. Donc, en gros, cela donne à C une pseudo fonctionnalité de première classe.

337voto

coobird Points 70356

Les pointeurs de fonction en C peuvent être utilisés pour réaliser 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 l'absence d'un new L'opérateur est un piège, mais il semble impliquer que nous définissons le texte d'un certain String la classe à être "hello" .

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

Comment cela se fait-il ?

El String est en fait une classe struct avec un tas de pointeurs de fonctions qui agissent comme un moyen de simuler des méthodes. Ce qui suit est une déclaration partielle de l'objet String classe :

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 constater, les méthodes de l String sont en fait des pointeurs de fonction vers la fonction déclarée. En préparant l'instance de la classe String le newString 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, le getString qui est appelée en invoquant la fonction get est définie comme suit :

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

Une chose que l'on peut remarquer est qu'il n'y a pas de concept d'instance d'un objet et qu'il n'y a pas de méthodes qui font réellement partie d'un objet, donc un "objet soi" doit être passé à chaque invocation. (Et le internal est juste un caché struct qui a été omis de la liste de code plus tôt -- c'est une façon d'effectuer le masquage de l'information, mais ce n'est pas pertinent pour les pointeurs de fonction).

Donc, plutôt que d'être capable de faire s1->set("hello"); il faut passer l'objet sur lequel l'action doit être effectuée. s1->set(s1, "hello") .

Maintenant que cette petite explication sur la nécessité de passer une référence à soi-même est réglée, nous allons passer à la partie suivante, à savoir héritage en C .

Disons que nous voulons créer une sous-classe de String , disons un ImmutableString . Afin de rendre la chaîne de caractères immuable, l'élément set ne sera pas accessible, tout en maintenant l'accès à la méthode get y length et de forcer le "constructeur" à accepter une valeur de 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 l'objet set n'est pas présente, elle ne peut donc pas être appelée dans une méthode ImmutableString .

Quant à la mise en œuvre de la ImmutableString le seul code pertinent est celui de la fonction "constructeur", la fonction 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;
}

Lors de l'instanciation de la ImmutableString les pointeurs de fonction vers les get y length font en fait référence à la String.get y String.length en passant par la méthode base qui est une variable stockée en interne String objet.

L'utilisation d'un pointeur de fonction permet d'hériter d'une méthode à partir d'une superclasse.

Nous pouvons en outre continuer à polymorphisme en C .

Si, par exemple, nous voulions modifier le comportement de la fonction length pour retourner 0 tout le temps dans le ImmutableString classe pour une raison quelconque, il suffirait de faire :

  1. Ajoutez une fonction qui servira de fonction de remplacement. length méthode.
  2. Allez dans le "constructeur" et placez le pointeur de fonction sur la fonction prioritaire length méthode.

Ajout d'une surcharge length méthode dans ImmutableString peut être effectuée en ajoutant un lengthOverrideMethod :

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

Ensuite, le pointeur de fonction pour le length dans le constructeur est reliée à la méthode 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 les length méthode dans ImmutableString comme la classe String maintenant la classe length fera référence au comportement défini dans la méthode lengthOverrideMethod fonction.

Je dois ajouter un avertissement : je suis encore en train d'apprendre à é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 sont peut-être simplement à côté de la plaque en ce qui concerne la meilleure façon d'implémenter la POO en C. Mais mon but était d'essayer d'illustrer une des nombreuses utilisations des pointeurs de fonction.

Pour plus d'informations sur la manière de réaliser une programmation orientée objet en C, veuillez vous référer aux questions suivantes :

30 votes

Cette réponse est horrible ! Non seulement elle implique que l'OO dépend d'une manière ou d'une autre de la notation par points, mais elle encourage également à mettre des déchets dans vos objets !

34 votes

C'est bien de l'OO, mais pas du tout de l'OO de style C. Ce que vous avez implémenté de manière cassée est un OO basé sur des prototypes de style Javascript. Pour obtenir un OO de style C++/Pascal, vous devez.. : 1. Avoir une structure constante pour une table virtuelle de chaque type d'objet. classe avec des membres virtuels. 2. Avoir un pointeur vers cette structure dans les objets polymorphes. 3. Appeler les méthodes virtuelles par l'intermédiaire de la table virtuelle, et toutes les autres méthodes directement -- généralement en s'en tenant à un certain nombre de règles. ClassName_methodName convention de dénomination des fonctions. Ce n'est qu'alors que vous obtiendrez les mêmes coûts d'exécution et de stockage qu'en C++ et en Pascal.

21 votes

Travailler en OO avec un langage qui n'est pas destiné à être OO est toujours une mauvaise idée. Si vous voulez de l'OO et que vous avez toujours le C, travaillez simplement avec le C++.

260voto

Lee Gao Points 27

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. 0xC3 est un x86 ret instruction .

Normalement, vous ne les écrivez pas à la main, vous les écrivez en langage d'assemblage et vous utilisez ensuite un assembleur tel que nasm pour l'assembler en un binaire plat que vous transformez en chaîne de caractères C.

  1. Renvoie la valeur actuelle du registre EAX.

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

    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 for-loop à 1000, en 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 les littéraux des chaînes de caractères dans la section .rodata (ou .rdata sur Windows), qui est lié en tant que partie du segment de texte (avec le code des fonctions).

Le segment de texte a le droit de lecture et d'exécution, de sorte que la conversion des chaînes de caractères en pointeurs de fonction fonctionne sans qu'il soit nécessaire d'avoir recours à l'option mprotect() o VirtualProtect() les appels système dont vous auriez besoin pour la mémoire allouée dynamiquement. (Ou gcc -z execstack lie le programme avec pile + segment de données + tas exécutable, comme un hack rapide).


Pour les désassembler, vous pouvez le 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";

Compilation avec gcc -c -m32 foo.c et le démontage avec objdump -D -rwC -Mintel nous pouvons obtenir l'assemblage, et découvrir que ce code viole l'ABI en bloquant EBX (un registre réservé aux 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 sur Windows, Linux, OS X, etc. : les conventions d'appel par défaut sur tous ces OS passent les args sur la pile au lieu de les passer plus efficacement dans les registres. Mais EBX est préservé dans toutes les conventions d'appel normales, donc l'utiliser comme registre scratch sans le sauvegarder/restaurer peut facilement faire planter l'appelant.

9 votes

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

1 votes

Cela fonctionnera-t-il avec Visual Studio ? J'obtiens:error C2440 : 'type cast' : cannot convert from 'const char [68]' to 'void (__cdecl *)(int *,int *)' . Une idée ?

5 votes

Salut Matt ! En fonction du niveau d'optimisation, GCC va souvent mettre en ligne les constantes de chaîne dans le segment TEXTE, donc cela fonctionnera même sur les versions plus récentes de Windows à condition que vous n'empêchiez pas ce type d'optimisation. (IIRC, la version de MINGW au moment de mon post il y a plus de deux ans met en ligne les chaînes de caractères au niveau d'optimisation par défaut).

122voto

Nick Points 8126

L'une de mes utilisations préférées des pointeurs de fonction est celle d'itérateurs simples et bon marché.

#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);
}

9 votes

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 les résultats des itérations (pensez aux fermetures).

3 votes

Approuvé. Tous mes itérateurs ressemblent à ça : int (*cb)(void *arg, ...) . La valeur de retour de l'itérateur me permet également de m'arrêter prématurément (si elle est non nulle).

25voto

Pointeurs de fonction facile à déclarer une fois que vous avez la base declarators:

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

Alors que D est une autre déclaration construit à l'aide de 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 la déclaration de l'entité. Nous allons essayer de construire une fonction prenant un pointeur vers une fonction prend rien et le retour de type int, et retournant un pointeur à une fonction prenant un char et le retour de type int. Avec type-defs 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, il n'est pas difficile, soit avec la déclaration de règles, appliquées de manière cohérente. Comme vous le voyez j'ai raté la partie sur laquelle pointe le pointeur, et la chose la fonction retourne. C'est ce qui apparaît à l'extrême gauche de la déclaration, et n'est pas d'intérêt: Il est ajouté à la fin si l'on construit le constate déjà. Faisons-le. Mettre en place de manière cohérente, premier long - montrant la structure à l'aide de [ 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 declarators l'une après l'autre. La Construction peut se faire de deux façons. On est bottom-up, à commencer par la très bonne chose (les feuilles) et sur le même chemin jusqu'à l'identificateur. L'autre façon est de haut en bas, en commençant à l'identifiant, le travail de la manière vers le bas pour les feuilles. Je vais vous montrer deux façons.

De Bas En Haut

La Construction commence par la chose à la droite: La chose retourné, ce qui est la fonction la prise de char. Pour garder la declarators distinctes, je vais les numéroter:

D1(char);

Inséré le char paramètre directement, puisqu'il est trivial. L'ajout d'un pointeur de déclaration par le remplacement, D1 par *D2. Notez que nous avons pour envelopper les parenthèses autour de *D2. Ce qui peut être connu par la recherche de la priorité de l' *-operator et la fonction d'appel de l'opérateur (). Sans notre parenthèses, le compilateur pourrait le lire comme *(D2(char p)). Mais ce ne serait pas un simple remplacement de D1 en *D2 plus, bien sûr. Les parenthèses sont toujours autorisés autour de declarators. Afin de ne pas faire quelque chose de mal si vous ajoutez trop d'entre eux, en fait.

(*D2)(char);

Le type de retour est complet! Maintenant, nous allons remplacer D2 par la fonction de demande de déclaration de fonction prenant <parameters> de retour, qui est - D3(<parameters>) qui nous sommes à maintenant.

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

Notez que les parenthèses sont nécessaires, car nous voulons D3 à une fonction de demande de déclaration et non pas un pointeur déclaration de ce temps. La grande, la seule chose qui reste est les paramètres pour cela. Le paramètre est en fait exactement la même chose que nous avons fait le type de retour, juste avec char remplacé par void. Donc je vais le copier:

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

J'ai remplacé D2 par ID1, puisque nous en avons terminé avec ce paramètre (c'est déjà un pointeur vers une fonction - pas besoin d'une autre demande de déclaration). ID1 "sera le nom du paramètre. Maintenant, j'ai dit ci-dessus à la fin on ajoute le type qui tous ceux déclaration de modifier, l'une apparaissant à l'extrême gauche de chaque déclaration. Pour les fonctions, qui devient le type de retour. Pour les pointeurs le fait de taper etc... C'est intéressant lorsqu'il est écrit en bas de la type, il apparaîtra dans l'ordre inverse, à droite :) de toute façon, en le substituant les rendements de la déclaration complète. Les deux fois, int de cours.

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

J'ai appelé l'identificateur de la fonction ID0 dans cet exemple.

De Haut En Bas

Cela commence à l'identifiant le plus à gauche dans la description du type, de l'emballage, qui constate que nous marchons notre chemin à travers le droit. Commencez avec fonction prenant <paramètres> de retour

ID0(<parameters>)

La prochaine chose que dans la description (d'après "le retour") a été pointeur. Nous allons l'intégrer:

*ID0(<parameters>)

Ensuite, la prochaine chose était due à la prise de <paramètres> de retour. Le paramètre est un simple char, afin de nous mettre tout de suite à nouveau, car il est vraiment trivial.

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

Notez les parenthèses, nous avons ajouté, depuis que nous voulons plus que l' * lie d'abord, et ensuite l' (char). Sinon, il serait de lire en fonction prenant <paramètres> le retour de fonction .... Rex, fonctions retournant des fonctions qui ne sont même pas autorisés.

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

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

Vient de mettre int avant la declarators comme nous l'avons fait avec bottom-up, et nous sommes finis

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

La bonne chose

Est bottom-up ou top-down de mieux? J'ai l'habitude de bottom-up, mais certaines personnes peuvent être plus à l'aise avec de haut en bas. C'est une question de goût je pense. D'ailleurs, si vous appliquez tous les opérateurs dans cette déclaration, vous finirez toujours par un entier (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 à l'aide de l'identifiant, puis il donne le type de la plus à gauche. C'est comme ça pour les tableaux de trop.

Espère que vous avez aimé ce petit tutoriel! Maintenant, nous pouvons lien vers cette quand les gens s'interroger sur l'étrange syntaxe de déclaration de fonctions. J'ai essayé de mettre un peu de C internals que possible. N'hésitez pas à modifier/corriger les choses.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X