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.
c
function-pointers
Yuval Adam
la source
la source
Réponses:
Pointeurs de fonction en C
Commençons par une fonction de base sur laquelle nous allons pointer :
Tout d'abord, définissons un pointeur sur une fonction qui reçoit 2
int
s et renvoie unint
:Maintenant, nous pouvons pointer en toute sécurité vers notre fonction:
Maintenant que nous avons un pointeur sur la fonction, utilisons-le:
Passer le pointeur à une autre fonction est fondamentalement le même:
Nous pouvons également utiliser des pointeurs de fonction dans les valeurs de retour (essayez de suivre, cela devient compliqué):
Mais c'est beaucoup plus agréable d'utiliser un
typedef
:la source
pshufb
, c'est lent, donc l'implémentation antérieure est toujours plus rapide. x264 / x265 l'utilisent largement et sont open source.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:
Oui, le
->
et le manque d'unnew
opérateur est un abandon mort, mais cela semble impliquer que nous définissons le texte d'uneString
classe"hello"
.En utilisant des pointeurs de fonction, il est possible d'imiter les méthodes en C .
Comment est-ce accompli?
La
String
classe est en fait unstruct
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 laString
classe:Comme on peut le voir, les méthodes de la
String
classe sont en fait des pointeurs de fonction vers la fonction déclarée. Lors de la préparation de l'instance deString
, lanewString
fonction est appelée afin de configurer les pointeurs de fonction vers leurs fonctions respectives:Par exemple, la
getString
fonction appelée en appelant laget
méthode est définie comme suit: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
internal
c'est juste un cachéstruct
qui 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'actions1->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 anImmutableString
. Afin de rendre la chaîne immuable, laset
méthode ne sera pas accessible, tout en maintenant l'accès àget
etlength
, et en forçant le "constructeur" à accepter achar*
:Fondamentalement, pour toutes les sous-classes, les méthodes disponibles sont à nouveau des pointeurs de fonction. Cette fois, la déclaration de la
set
méthode n'est pas présente, par conséquent, elle ne peut pas être appelée dans aImmutableString
.Quant à l'implémentation de la
ImmutableString
, le seul code pertinent est la fonction "constructeur", lanewImmutableString
:En instanciant le
ImmutableString
, la fonction pointe vers les méthodesget
etlength
fait référence à la méthodeString.get
etString.length
, en parcourant labase
variable qui est unString
objet 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
length
méthode pour retourner0
tout le temps dans laImmutableString
classe pour une raison quelconque, tout ce qu'il faudrait faire est de:length
méthode prioritaire .length
méthode prioritaire .L'ajout d'une
length
méthode prioritaire dansImmutableString
peut être effectué en ajoutant unlengthOverrideMethod
:Ensuite, le pointeur de fonction pour la
length
méthode dans le constructeur est connecté aulengthOverrideMethod
:Maintenant, plutôt que d'avoir un comportement identique pour la
length
méthode enImmutableString
classe comme laString
classe, maintenant lalength
méthode se référera au comportement défini dans lalengthOverrideMethod
fonction.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:
la source
ClassName_methodName
convention 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.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 uneret
instruction x86 .Normalement, vous ne les écrivez pas à la main, vous écrivez en langage assembleur, puis vous utilisez un assembleur comme
nasm
pour l'assembler dans un binaire plat que vous hexdumpez dans un littéral de chaîne C.Renvoie la valeur actuelle dans le registre EAX
Écrire une fonction de swap
Ecrire un compteur de boucles à 1000, appelant une fonction à chaque fois
Vous pouvez même écrire une fonction récursive qui compte jusqu'à 100
Notez que les compilateurs placent des littéraux de chaîne dans la
.rodata
section (ou.rdata
sous 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()
ouVirtualProtect()
appels système comme vous auriez besoin pour la mémoire allouée dynamiquement. (Ougcc -z execstack
relie 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.
En compilant
gcc -c -m32 foo.c
et en désassemblant avecobjdump -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.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.
la source
Une de mes utilisations préférées pour les pointeurs de fonction est comme itérateurs bon marché et faciles -
la source
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).Les pointeurs de fonction deviennent faciles à déclarer une fois que vous disposez des déclarants de base:
ID
: ID est*D
: pointeur DD(<parameters>)
: fonction D prise<
paramètres de>
retourAlors 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 çaComme 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]
: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:
Inséré directement le paramètre char, car il est trivial. Ajout d'un pointeur sur le déclarant en le remplaçant
D1
par*D2
. Notez que nous devons entourer les parenthèses*D2
. Cela peut être connu en recherchant la priorité de l'*-operator
opé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.Le type de retour est terminé! Maintenant, remplaçons
D2
par la fonction déclarant la fonction prenant<parameters>
return , qui estD3(<parameters>)
ce que nous sommes maintenant.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 avecchar
remplacé parvoid
. Je vais donc le copier:J'ai remplacé
D2
parID1
, car nous avons terminé 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, 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 foisint
bien sûr.J'ai appelé l'identifiant de la fonction
ID0
dans 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>
renvoyantLa prochaine chose dans la description (après "retour") était le pointeur sur . Incorporons-le:
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.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.Mettez juste
int
devant les déclarants comme nous l'avons fait de bas en haut, et nous avons terminé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:
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.
la source
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
printf
pour 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
Dans
version.c
Je définirai la fonction 2 prototypes présents dansversion.h
version.c
Remarquez comment le pointeur de fonction est prototypé en
version.h
tant quevoid (* 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 laboard_init()
fonction à laquellezprintf
est 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ébogageou
zprintf = &noprint;
zprintf retourne simplement et n'exécutera pas de code inutileL'exécution du code ressemblera à ceci:
mainProg.c
Le code ci-dessus utilisera
printf
s'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 versionversion.h
et le code fera le reste!la source
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 donne juste un exemple complet:
la source
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
qsort
etbsearch
qui 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 (soitadd()
ousub()
, qui sont du même type) alors ce que nous ferons, nous ajouterons un argument de pointeur de fonction à laprint()
fonction comme indiqué ci-dessous :La sortie est:
la source
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.
Aprè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.
la source
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:
Les deux premières déclarations sont quelque peu similaires en ce sens:
func
est une fonction qui prend unint
et unchar *
et renvoie unint
pFunc
est un pointeur de fonction auquel est affectée l'adresse d'une fonction qui prend unint
et unchar *
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 fonctionpFunc
comme danspFunc = 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.
Plusieurs exemples d'utilisation différents
Quelques exemples d'utilisation d'un pointeur de fonction:
Vous pouvez utiliser des listes de paramètres de longueur variable dans la définition d'un pointeur de fonction.
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.
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.
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
if
instruction, mais je ne sais pas à quel point cela serait utile. D'autres opérateurs de comparaison semblent avoir encore moins d'utilité.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
void
qui 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:Style C
namespace
Utilisation de Globalstruct
avec des pointeurs de fonctionVous pouvez utiliser le
static
mot - 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 à lanamespace
fonctionnalité 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.
Puis dans le fichier source C:
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
const
modificateur est utilisé sur le global pour qu'il ne puisse pas être modifié par accident.Domaines d'application des pointeurs de fonction
Un composant de bibliothèque DLL pourrait faire quelque chose de similaire à l'
namespace
approche 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'unestruct
fonction 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.et cela pourrait être utilisé comme dans:
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()
etbsearch()
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 ++.
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.
la source
É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 :)
la source