J'ai eu une expérience récente avec les pointeurs de fonction en C.
Alors, continuant avec 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 dans le sujet.
Commençons par une fonction de base que nous allons être montrant:
int addInt(int n, int m) {
return n+m;
}
Tout d’abord, définissons un pointeur sur une fonction qui reçoit 2 int
s et retourne un int
:
int (*functionPtr)(int,int);
Nous pouvons maintenant indiquer notre fonction en toute sécurité:
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 les 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 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;
}
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 l’absence d’un opérateur new
est un don mort, mais cela semble impliquer que nous définissions le texte de certaines classes de la classe String
comme étant "hello"
.
En utilisant des pointeurs de fonction, , il est possible d'émuler des méthodes en C .
Comment est-ce accompli?
La classe String
est en fait une struct
avec un tas de pointeurs de fonction qui permettent de simuler des méthodes. Ce qui suit est une déclaration partielle de la classe String
:
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 la classe String
sont en réalité des pointeurs de fonction sur la fonction déclarée. Lors de la préparation de l'instance de String
, la fonction newString
est appelée afin de configurer les pointeurs de fonction sur 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 fonction getString
appelée en appelant la méthode 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’existe pas de concept d’instance d’objet et que les méthodes font en fait partie intégrante d’un objet. Par conséquent, un "objet autonome" doit être transmis à chaque appel. (Et le internal
est juste un struct
masqué qui a été omis de la liste de codes précédemment - c'est un moyen de masquer des informations, mais cela ne concerne pas les pointeurs de fonction.)
Ainsi, plutôt que de pouvoir faire s1->set("hello");
, il faut passer l'objet pour effectuer l'action sur s1->set(s1, "hello")
.
Cette explication mineure devant passer par une référence à vous-même, nous passons à la partie suivante, qui est l'héritage en C .
Disons que nous voulons faire une sous-classe de String
, disons un ImmutableString
. Afin de rendre la chaîne immuable, la méthode set
ne sera pas accessible, tout en conservant l'accès à get
et length
, et obligera 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 de la méthode set
n'est pas présente. Par conséquent, elle ne peut pas être appelée dans un ImmutableString
.
En ce qui concerne l'implémentation de 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 la ImmutableString
, la fonction pointe sur les méthodes get
et length
se réfèrent à la méthode String.get
et String.length
en passant par la méthode base
variable qui est un objet String
stocké en interne.
L'utilisation d'un pointeur de fonction peut permettre l'héritage d'une méthode d'une super-classe.
On peut continuer à polymorphisme en C .
Si, par exemple, nous voulions changer le comportement de la méthode length
pour renvoyer 0
tout le temps dans la classe ImmutableString
pour une raison quelconque, il suffirait de:
length
.length
de substitution.L'ajout d'une méthode length
prioritaire dans ImmutableString
peut être effectué en ajoutant un lengthOverrideMethod
:
int lengthOverrideMethod(const void* self)
{
return 0;
}
Ensuite, le pointeur de fonction pour la méthode length
dans le constructeur est relié à 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 la méthode length
dans la classe ImmutableString
en tant que classe String
, la méthode length
fera maintenant référence au comportement défini dans la classe lengthOverrideMethod
fonction.
Je dois ajouter un avertissement selon lequel j'apprends toujours à écrire avec un style de programmation orienté objet en C, de sorte qu'il y a probablement des points que je n'ai pas bien expliqués ou que je ne suis peut-être pas à la hauteur en ce qui concerne la meilleure mise en œuvre OOP en C. Mais mon but était d'essayer d'illustrer l'une des nombreuses utilisations des pointeurs de fonction.
Pour plus d'informations sur l'exécution de la programmation orientée objet en C, reportez-vous aux questions suivantes:
Le guide pour se faire virer: Comment abuser des pointeurs de fonction dans GCC sur des 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 ne instruction x86 ret
.
Normalement, vous ne les écririez pas à la main, vous utiliseriez un assembleur tel que nasm
pour l'assembler en un binaire plat que vous hexdumperiez dans un littéral de chaîne C.
Retourne la valeur actuelle sur le registre EAX
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
Écrire 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);
Écrire un compteur de boucle for à 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
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 section .rodata
(ou .rdata
sur Windows), qui est liée au segment de texte (avec le code pour les fonctions).
Le segment de texte dispose des autorisations Read + Exec. Par conséquent, la conversion des littéraux de chaîne en pointeurs de fonction fonctionne sans avoir besoin de mprotect()
ou VirtualProtect()
appels système comme vous auriez besoin d'une mémoire allouée dynamiquement. (Ou gcc -z execstack
lie le programme avec une pile + un segment de données + un 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 avec gcc -c -m32 foo.c
et en désassemblant avec objdump -D -rwC -Mintel
, nous pouvons obtenir l’Assembly et découvrir que ce code viole l’ABI en écrasant EBX (un registre à conservation d’appel) et qu’il 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, etc.: les conventions d’appel par défaut sur tous ces systèmes d’exploitation transmettent les arguments sur la pile au lieu d’être plus efficaces dans les registres. Mais EBX est préservé dans toutes les conventions d’appel habituelles. Par conséquent, son utilisation en tant que registre de travail sans sauvegarde/restauration peut facilement provoquer le blocage de l’appelant.
L’une de mes utilisations préférées des pointeurs de fonction est la possibilité de faire des itérateurs faciles et peu coûteux -
#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);
}
Les pointeurs de fonction deviennent faciles à déclarer une fois que vous avez les déclarateurs de base:
ID
: ID est a*D
: pointeur D surD(<parameters>)
: fonction D prenant <
parameters>
retournantAlors que D est un autre déclarateur construit en utilisant ces mêmes règles. En fin de compte, quelque part, il se termine par ID
(voir l'exemple ci-dessous), qui correspond au nom de l'entité déclarée. Essayons de construire une fonction prenant un pointeur sur une fonction ne prenant rien et retournant int, et retournant un pointeur sur une fonction prenant un caractère et retournant int. Avec les caractères typographiques 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 oublié la partie sur laquelle pointe le pointeur et la chose retournée par la fonction. C'est ce qui apparaît à l'extrême gauche de la déclaration et n'a aucun intérêt: il est ajouté à la fin si on a déjà construit le déclarant. Faisons cela. Construisez-le 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, il est possible de décrire complètement un type en ajoutant des déclarants les uns après les autres. La construction peut être faite de deux manières. L'une est ascendante, commence par la bonne chose (feuilles) et continue jusqu'à l'identifiant. L’autre moyen est de haut en bas, en partant de l’identificateur, en descendant vers les feuilles. Je vais montrer les deux manières.
La construction commence par la chose à droite: la chose est retournée, c'est-à-dire la fonction prenant char. Pour séparer les déclarants, je vais les numéroter:
D1(char);
Inséré le paramètre char directement, car il est trivial. Ajout d'un pointeur au déclarateur en remplaçant D1
par *D2
. Notez que nous devons placer des parenthèses autour de *D2
. Cela peut être connu en recherchant la priorité de *-operator
et de l'opérateur d'appel de fonction ()
. Sans nos parenthèses, le compilateur le lirait comme suit: *(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 complet! Maintenant, remplaçons D2
par le déclarateur de fonction fonction prenant <parameters>
retournant, qui est D3(<parameters>)
à laquelle nous en sommes maintenant.
(*D3(<parameters>))(char)
Notez qu'aucune parenthèse n'est nécessaire, puisque nous voulonsD3
être un déclarateur de fonction et non un déclarateur de pointeur cette fois. Génial, il ne reste que les paramètres pour cela. Le paramètre est identique à celui du type de retour, avec char
remplacé par void
. Donc je vais le copier:
(*D3( (*ID1)(void)))(char)
J'ai remplacé D2
par ID1
, car nous en avons terminé avec ce paramètre (c'est déjà un pointeur sur une fonction - pas besoin d'un autre déclarateur). ID1
sera le nom du paramètre. Maintenant, j'ai dit plus haut, à la fin, on ajoute le type modifié par tous les déclarateurs - celui qui apparaît à l'extrême gauche de chaque déclaration. Pour les fonctions, cela devient le type de retour. Pour les pointeurs, le type pointé, etc ... C'est intéressant quand on écrit le type, il apparaîtra dans l'ordre inverse, à l'extrême droite :) Quoi qu'il en soit, le remplacer par 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.
Cela commence à l'identifiant tout à gauche dans la description du type, en enveloppant ce déclarant alors que nous marchons à droite. Commencez par fonction prenant <
parameters>
retournant
ID0(<parameters>)
La prochaine chose dans la description (après "return") était pointeur sur. Intégrons-le:
*ID0(<parameters>)
Ensuite, la chose suivante était fonction prenant <
parameters>
retournant. Le paramètre est un caractère simple, nous l'avons donc tout de suite inséré, car il est vraiment trivial.
(*ID0(<parameters>))(char)
Notez les parenthèses que nous avons ajoutées, car nous voulons à nouveau que le *
soit lié en premier et then le (char)
. Sinon, il faudrait lire fonction prenant <
parameters>
fonction de retour .... Non, les fonctions renvoyant des fonctions ne sont même pas autorisées.
Maintenant, nous avons juste besoin de mettre <
parameters>
. Je vais montrer une version courte de la dérivation, car je pense que vous avez déjà maintenant l’idée de le faire.
pointer to: *ID1
... function taking void returning: (*ID1)(void)
Il suffit de mettre int
devant les déclarateurs, comme nous l’avons fait avec la méthode ascendante, et nous avons terminé.
int (*ID0(int (*ID1)(void)))(char)
L'approche ascendante ou descendante est-elle préférable? J'ai l'habitude de faire 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. Incidemment, si vous appliquez tous les opérateurs de cette déclaration, vous obtiendrez un int:
int v = (*ID0(some_function_pointer))(some_char);
C’est une propriété de Nice des déclarations en C: la déclaration affirme que si ces opérateurs sont utilisés dans une expression utilisant l’identificateur, le type est renvoyé à l'extrême gauche. C'est comme ça pour les tableaux aussi.
J'espère que vous avez aimé ce petit tutoriel! Nous pouvons maintenant établir un lien avec ceci lorsque des personnes s'interrogent sur l'étrange syntaxe de déclaration des fonctions. J'ai essayé de mettre le moins possible de composants internes. N'hésitez pas à éditer/corriger les choses en elle.
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 (doté de ports pour les affichages et les consoles, mais ils ne sont pas nécessaires/recherchés pour la version finale). Donc, pendant le développement, je vais utiliser printf
pour voir les messages d’état et d’erreur, mais quand j’aurai terminé, je ne veux plus rien imprimer. Voici ce que j'ai fait:
// 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 définirai les 2 prototypes de fonctions présents dans version.h
#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é dans version.h
comme
void (* zprintf)(const char *, ...);
Lorsqu'il est référencé dans l'application, il commence à s'exécuter où qu'il se trouve, ce qui n'a pas encore été défini.
Dans version.c
, remarquez dans la fonction board_init()
où zprintf
est affectée à une fonction unique (dont la signature de 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 simplement et n'exécutera pas de code inutile
L'exécution du code ressemblera à ceci:
#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 vous êtes en mode débogage ou ne faites rien s'il est en mode relâchement. C'est beaucoup plus facile que de parcourir l'ensemble du projet et de commenter ou supprimer du code. Tout ce que je dois faire est de changer la version dans version.h
et le code fera le reste!
Le pointeur de fonction est généralement défini par typedef
et utilisé comme paramètre et valeur de retour.
Les réponses ci-dessus ont déjà beaucoup expliqué, je viens de donner 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 ∑
}
// test - use function pointer as variable,
void test_pointer_as_variable() {
// create a pointer to function,
two_num_operation sum_p = ∑
// 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;
}
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 comporte deux routines, qsort
et bsearch
, qui prend un pointeur sur une fonction appelée pour comparer deux éléments en cours de tri; Cela vous permet de trier ou de rechercher, n'importe quoi, en fonction des critères que vous souhaitez utiliser.
Un exemple très basique, s’il existe une fonction appelée print(int x, int y)
qui, à son tour, peut nécessiter l’appel d’une fonction (soit add()
, soit sub()
, qui sont du même type). Nous allons ajouter un argument de pointeur de fonction à la fonction print()
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 de: 390
Un pointeur de fonction est une variable qui contient l'adresse d'une fonction. Puisqu'il s'agit d'une variable de pointeur avec certaines propriétés restreintes, vous pouvez l'utiliser à peu près comme 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 indiquant autre chose qu'une valeur unique. 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 ex. 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 puisse effectuer les types de vérifications habituels. 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 existe des cas où ce manque de vérification peut être utile, mais rappelez-vous 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 assez similaires en ce que:
func
est une fonction qui prend un int
et un char *
et renvoie un int
pFunc
est un pointeur de fonction auquel est attribuée l'adresse d'une fonction qui prend un int
et un char *
et renvoie un int
Ainsi, à 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 pFunc
comme dans pFunc = func;
.
Notez la syntaxe utilisée avec une déclaration/définition de pointeur de fonction dans laquelle les parenthèses sont utilisées pour s'affranchir des 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 spécifier une liste de paramètres du tout. Cela peut être utile mais é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);
Cts de style C
Vous pouvez utiliser des conversions de style C avec des pointeurs de fonction. Cependant, sachez qu'un compilateur C peut être laxiste en matière de vérification 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.
Pointeur de fonction de comparaison à égalité
Vous pouvez vérifier qu'un pointeur de fonction est égal à une adresse de fonction particulière à l'aide d'une instruction if
même si je ne suis pas sûr de son utilité. 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;
}
n tableau de pointeurs de fonction
Et si vous souhaitez disposer d'un tableau de pointeurs de fonction, chacun des éléments pour lesquels la liste d'arguments présente des différences, vous pouvez définir un pointeur de fonction avec la liste d'arguments non spécifiée (pas void
, ce qui signifie qu'aucun argument, mais uniquement, n'a pas été spécifiée). vous pouvez voir des avertissements du compilateur C. Cela fonctionne également pour un paramètre de pointeur de fonction sur 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 namespace
Utilisation de struct
global avec des pointeurs de fonction
Vous pouvez utiliser le mot clé static
pour spécifier une fonction dont le nom correspond à la portée du fichier, puis l'affecter à une variable globale afin de fournir quelque chose de similaire à la fonctionnalité namespace
de C++.
Dans un fichier d'en-tête, définissez une structure qui sera notre espace de noms avec une variable globale qui l'utilise.
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};
Ceci 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 modificateur const
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 effectuer quelque chose de similaire à l'approche namespace
du style C dans laquelle une interface de bibliothèque particulière est demandée à une méthode de fabrique dans une interface de bibliothèque prenant en charge la création d'un struct
contenant des pointeurs de fonction .. Cette bibliothèque interface charge la version demandée DLL, crée une structure avec les pointeurs de fonction nécessaires, puis renvoie la structure à l'appelant demandeur.
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 un code utilisant un modèle particulier du matériel sous-jacent. Les pointeurs de fonction sont dotés 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. Ceci 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 sur 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 les pointeurs de fonction pour déléguer une tâche ou une fonctionnalité. L'exemple classique en C est le pointeur de la fonction de délégué de comparaison utilisé avec les fonctions de bibliothèque Standard C qsort()
et bsearch()
pour fournir l'ordre de classement permettant de trier une liste d'éléments ou d'effectuer une recherche binaire sur une liste triée de articles. 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 C++ Standard Template Library.
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. La structure Microsoft MFC avec ses mappes de messages utilise quelque chose de similaire pour gérer les messages Windows remis à une fenêtre ou à un thread.
Les fonctions asynchrones nécessitant 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 complétant sa tâche.
La fonction de démarrage à partir de zéro a une adresse mémoire à partir de laquelle ils commencent à s'exécuter. Dans le langage d'assemblage, elles sont appelées comme (appelez "l'adresse mémoire de la fonction"). Revenez maintenant en C Si la fonction a une adresse mémoire, elles peuvent être manipulées par des pointeurs en C. Par les règles de C
1.Vous devez d'abord déclarer un pointeur sur la fonction 2. Passer 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");
}
Après que cela laisse voir comment la machine comprend Them.Glimpse d'instruction machine du programme ci-dessus dans une architecture 32 bits.
La zone marquée en rouge indique 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.
Étant donné que les pointeurs de fonction sont souvent des rappels dactylographiés, vous pouvez jeter un oeil à tapez des rappels sécurisés . La même chose s'applique aux points d'entrée, etc. des fonctions qui ne sont pas des rappels.
C est assez capricieux et pardonnant en même temps :)