Question Comment fonctionnent les pointeurs de fonction en C?


J'ai eu un peu d'expérience ces derniers temps avec des pointeurs de fonction en C.

Continuant ainsi la tradition de répondre à vos propres questions, j'ai décidé de faire un petit résumé des bases mêmes, pour ceux qui ont besoin d'un rapide plongeon dans le sujet.


1006
2018-05-08 15:49


origine


Réponses:


Les pointeurs de fonction en C

Commençons par une fonction de base que nous serons pointant vers:

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 en toute sécurité pointer 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 vers 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 garder le rythme, ça devient salissant):

// 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;
}

1245
2018-05-08 15:49



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 new opérateur est un don mort, mais il semble bien impliquer que nous définissons le texte de certains String classe à être "hello".

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

Comment cela est-il accompli?

le String classe est en fait un struct avec 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 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 voir, les méthodes de String classe sont en fait des pointeurs de fonction à la fonction déclarée. En préparant l'instance du String, la newString La fonction est appelée pour configurer les pointeurs de fonction à 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 fonction qui est appelée en invoquant la get La méthode 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'une instance d'un objet et que des méthodes font réellement partie d'un objet, donc un "self objet" doit être passé à chaque invocation. (Et le internal est juste un caché struct qui a été omis de la liste de codes précédente - c'est une façon de cacher des informations, mais cela n'est pas pertinent pour les pointeurs de fonction.)

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

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

Disons que nous voulons faire une sous-classe de String, dis un ImmutableString. Afin de rendre la chaîne immuable, le set la méthode ne sera pas accessible, tout en maintenant l'accès à get et lengthet force le "constructeur" à accepter un 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 pour le set méthode n'est pas présente, par conséquent, il ne peut pas être appelé dans un ImmutableString.

Quant à la mise en œuvre de la ImmutableString, le seul code pertinent est la fonction "constructeur", le 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, les pointeurs de fonction vers get et length les méthodes se réfèrent effectivement à la String.get et String.length méthode, en passant par le base variable qui est stockée en interne String objet.

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

Nous pouvons continuer à continuer à polymorphisme en C.

Si par exemple nous voulions changer le comportement du length méthode pour retourner 0 tout le temps dans le ImmutableString classe pour une raison quelconque, tout ce qui devrait être fait est de:

  1. Ajouter une fonction qui va servir de priorité length méthode.
  2. Allez dans le "constructeur" et réglez le pointeur de fonction sur le dépassement length méthode.

Ajouter un overriding length méthode dans ImmutableString peut être effectué en ajoutant un lengthOverrideMethod:

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

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

Je dois ajouter un avertissement que j'apprends encore à é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 pourraient être hors de propos en ce qui concerne la meilleure façon de mettre en œuvre 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 programmation en C orientée objet, veuillez vous référer aux questions suivantes:


267



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

  1. Renvoie la valeur actuelle sur le registre EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
    
  2. Ecrire une fonction d'échange

    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 boucle for à 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);
    

187



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

95



Les pointeurs de fonction deviennent faciles à déclarer une fois que vous avez les déclarateurs de base:

  • id: ID: L'identifiant est un
  • Aiguille: *D: D pointeur vers
  • Fonction: D(<parameters>): D prise de fonction <paramètres> retour

Alors que D est un autre déclarateur construit en utilisant ces mêmes règles. À la fin, 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 en prenant un pointeur sur une fonction ne prenant rien et retournant int, et retournant un pointeur sur une fonction prenant un caractère char et retournant 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 construire en utilisant typedefs. Sans typedefs, ce n'est pas difficile non plus avec les règles du déclarant ci-dessus, appliquées de manière cohérente. Comme vous voyez, j'ai raté la partie pointée par le pointeur, et la chose que la fonction retourne. C'est ce qui apparaît tout à fait à gauche de la déclaration, et n'est pas intéressant: il est ajouté à la fin si l'on a déjà créé le déclarateur. Faisons cela. Construire de façon cohérente, d'abord verbeux - 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 être faite de deux façons. L'un est de bas en haut, en commençant par la bonne chose (les feuilles) et en progressant jusqu'à l'identifiant. L'autre façon est top-down, en commençant par l'identifiant, en descendant vers les feuilles. Je vais montrer les deux façons.

De bas en haut

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

D1(char);

Inséré le paramètre char directement, car c'est trivial. Ajouter un pointeur au déclarateur en remplaçant D1 par *D2. Notez que nous devons entourer les parenthèses *D2. Cela peut être connu en recherchant la préséance de la *-operator et l'opérateur d'appel de fonction (). Sans nos parenthèses, le compilateur le lirait comme *(D2(char p)). Mais ce ne serait pas un remplacement de D1 par *D2 plus, bien sûr. Les parenthèses sont toujours autorisées autour des déclarateurs. Donc, vous ne faites rien de mal si vous en ajoutez trop, en fait.

(*D2)(char);

Le type de retour est complet! Maintenant, remplaçons D2 par le déclarateur de fonction prise de fonction <parameters> retour, lequel est D3(<parameters>) que nous sommes maintenant.

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

Notez qu'aucune parenthèse n'est nécessaire, puisque nous vouloir  D3 être un déclarateur de fonction et pas un déclarateur de pointeur cette fois. Super, il ne reste que les paramètres pour cela. Le paramètre est 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 avons fini avec ce paramètre (c'est déjà un pointeur vers une fonction - pas besoin d'un autre déclarant). ID1 sera le nom du paramètre. Maintenant, je l'ai dit plus haut à la fin on ajoute le type que tous ces déclarants modifient - celui qui apparaît à la toute gauche de chaque déclaration. Pour les fonctions, cela devient le type de retour. Pour les pointeurs le pointé vers le type etc ... C'est intéressant quand on écrit le type, il apparaîtra dans l'ordre inverse, tout à fait à droite :) En tout cas, le substituer donne la déclaration complète. Les deux fois int bien sûr.

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

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

De haut en bas

Cela commence à l'identifiant tout à fait à gauche dans la description du type, enveloppant ce déclarant pendant que nous marchons notre chemin à travers le droit. Commencer avec prise de fonction <paramètres> retour

ID0(<parameters>)

La prochaine chose dans la description (après "retour") était pointeur vers. Allons-y:

*ID0(<parameters>)

Ensuite, la chose suivante était functon prenant <paramètres> retour. Le paramètre est un simple caractère, donc nous le remettons tout de suite, car c'est vraiment trivial.

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

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

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

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

Mettez juste int avant les déclarants comme nous l'avons fait avec bottom-up, et nous avons terminé

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

La bonne chose

Est-ce que le bottom-up ou le top-down sont meilleurs? Je suis habitué à faire de la base, mais certaines personnes peuvent être plus à l'aise avec une approche descendante. C'est une question de goût je pense. Incidemment, 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é des déclarations en C: La déclaration affirme que si ces opérateurs sont utilisés dans une expression en utilisant l'identifiant, alors il donne le type tout à fait à gauche. C'est comme ça pour les tableaux aussi.

J'espère que vous avez aimé ce petit tutoriel! Maintenant, nous pouvons lier à cela quand les gens s'interrogent sur l'étrange syntaxe de déclaration des fonctions. J'ai essayé de mettre le moins de C internes possible. Ne hésitez pas à modifier / réparer les choses en elle.


23



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

Ils sont très pratiques à utiliser lorsque vous voulez des fonctions différentes à différents moments ou différentes phases de développement. Par exemple, je développe une application sur un ordinateur hôte qui a une console, mais la version finale du logiciel sera mise sur un Aved ZedBoard (qui a des ports pour les écrans et les consoles, mais ils ne sont pas nécessaires / voulus pour le version finale). Donc, pendant le développement, je vais utiliser printf pour voir les messages d'état et d'erreur, mais quand j'ai fini, 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.c Je vais définir les 2 prototypes de fonction présents dans version.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;
}

Notez comment le pointeur de fonction est prototypé dans version.h comme

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

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

Dans version.c, remarquez dans le board_init()fonction où zprintf est affecté une fonction unique (dont la signature de la fonction correspond) en fonction de la version définie dans version.h

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

ou

zprintf = &noprint zprintf retourne juste 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 printf si en mode débogage, ou ne rien faire si en mode de libération. C'est beaucoup plus facile que de parcourir tout le projet et de commenter ou de supprimer du code. Tout ce que je dois faire est de changer la version version.h et le code fera le reste!


21



Le pointeur de fonction est généralement défini par typedef, et utilisé comme valeur de param & return,

Au-dessus des réponses déjà beaucoup expliquées, 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;
}

13