Come funzionano i puntatori a funzione in C?


1234

Ultimamente ho avuto qualche esperienza con i puntatori a funzione in C.

Quindi, continuando con la tradizione di rispondere alle tue domande, ho deciso di fare un piccolo riassunto delle basi, per coloro che hanno bisogno di un rapido approfondimento sull'argomento.


35
Inoltre: per un'analisi approfondita dei puntatori C, consultare blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Inoltre, la programmazione da zero mostra come funzionano a livello di macchina. Comprendere il "modello di memoria" di C è molto utile per capire come funzionano i puntatori C.
Abbafei,

8
Informazioni fantastiche. Dal titolo, però, mi sarei aspettato di vedere davvero una spiegazione di come "funzionano i puntatori a funzione", non come sono codificati :)
Bogdan Alexandru,

Risposte:


1478

Puntatori a funzioni in C

Cominciamo con una funzione di base che saremo indicandoci a :

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

Per prima cosa, definiamo un puntatore a una funzione che riceve 2 se intrestituisce un int:

int (*functionPtr)(int,int);

Ora possiamo tranquillamente indicare la nostra funzione:

functionPtr = &addInt;

Ora che abbiamo un puntatore alla funzione, usiamola:

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

Passare il puntatore a un'altra funzione è sostanzialmente lo stesso:

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

Possiamo usare anche i puntatori a funzione nei valori di ritorno (prova a tenere il passo, diventa disordinato):

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

Ma è molto più bello usare un typedef:

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

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

19
Grazie per le ottime informazioni. Potresti aggiungere alcune informazioni su dove vengono utilizzati i puntatori a funzione o risultano particolarmente utili?
Rich.Carpenter

326
"functionPtr = & addInt;" può anche essere scritto (e spesso lo è) come "functionPtr = addInt;" che è anche valido poiché lo standard dice che un nome di funzione in questo contesto viene convertito nell'indirizzo della funzione.
hlovdal

22
hlovdal, in questo contesto è interessante spiegare che questo è ciò che consente di scrivere functionPtr = ****************** addInt;
Johannes Schaub -

105
@ Rich.Carpenter So che è troppo tardi di 4 anni, ma immagino che altre persone potrebbero trarne beneficio: i puntatori a funzione sono utili per passare funzioni come parametri ad altre funzioni . Ho impiegato molte ricerche per trovare quella risposta per qualche strana ragione. Quindi, in sostanza, offre funzionalità pseudo di prima classe.
giant91,

22
@ Rich.Carpenter: i puntatori a funzione sono utili per il rilevamento della CPU di runtime. Avere più versioni di alcune funzioni per sfruttare SSE, popcnt, AVX, ecc. All'avvio, impostare i puntatori di funzione sulla versione migliore di ciascuna funzione per la CPU corrente. Nell'altro codice, basta chiamare tramite il puntatore a funzione invece di avere rami condizionali sulle funzionalità della CPU ovunque. Quindi puoi fare una logica complicata nel decidere bene, anche se questa CPU supporta pshufb, è lenta, quindi l'implementazione precedente è ancora più veloce. x264 / x265 lo usano ampiamente e sono open source.
Peter Cordes,

304

I puntatori di funzione in C possono essere utilizzati per eseguire la programmazione orientata agli oggetti in C.

Ad esempio, le seguenti righe sono scritte in C:

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

Sì, la ->mancanza di un newoperatore è un dono mortale, ma sembra implicare che stiamo impostando il testo di una Stringclasse "hello".

Utilizzando puntatori a funzione, è possibile emulare metodi C .

Come viene realizzato?

La Stringclasse è in realtà un structgruppo di puntatori a funzioni che agiscono come un modo per simulare metodi. Quella che segue è una dichiarazione parziale della Stringclasse:

typedef struct String_Struct* String;

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

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

String newString();

Come si può vedere, i metodi della Stringclasse sono in realtà puntatori di funzione alla funzione dichiarata. Nel preparare l'istanza di String, la newStringfunzione viene chiamata per impostare i puntatori di funzione alle rispettive funzioni:

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

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

    self->set(self, "");

    return self;
}

Ad esempio, la getStringfunzione chiamata richiamando il getmetodo è definita come segue:

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

Una cosa che si può notare è che non esiste il concetto di un'istanza di un oggetto e che abbia metodi che fanno effettivamente parte di un oggetto, quindi un "oggetto di sé" deve essere passato ad ogni invocazione. (E il internalsolo è un nascosto structche è stato omesso dall'elenco dei codici in precedenza - è un modo per eseguire la occultamento delle informazioni, ma questo non è rilevante per i puntatori a funzioni.)

Quindi, piuttosto che essere in grado di fare s1->set("hello");, è necessario passare l'oggetto per eseguire l'azione s1->set(s1, "hello").

Con questa spiegazione minore dover passare in un riferimento a se stessi fuori del modo, ci sposteremo alla parte successiva, che è l'ereditarietà in C .

Diciamo che vogliamo creare una sottoclasse di String, diciamo un ImmutableString. Per rendere immutabile la stringa, il setmetodo non sarà accessibile, pur mantenendo l'accesso a gete length, e forzando il "costruttore" ad accettare 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);

Fondamentalmente, per tutte le sottoclassi, i metodi disponibili sono ancora una volta puntatori a funzioni. Questa volta, la dichiarazione per il setmetodo non è presente, pertanto non può essere richiamata in a ImmutableString.

Per quanto riguarda l'implementazione di ImmutableString, l'unico codice rilevante è la funzione "costruttore", la newImmutableString:

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

    self->base = newString();

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

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

    return self;
}

Nell'istanza di ImmutableString, la funzione indica i metodi gete in lengthrealtà si riferiscono al metodo String.gete String.length, passando attraverso la basevariabile che è un Stringoggetto memorizzato internamente .

L'uso di un puntatore a funzione può ottenere l'ereditarietà di un metodo da una superclasse.

Possiamo inoltre continuare a polimorfismo in C .

Se per esempio volessimo cambiare il comportamento del lengthmetodo in modo che ritorni 0sempre nella ImmutableStringclasse per qualche motivo, tutto ciò che dovrebbe essere fatto è:

  1. Aggiungi una funzione che servirà come lengthmetodo di sostituzione .
  2. Vai al "costruttore" e imposta il puntatore a funzione sul lengthmetodo di sostituzione .

L'aggiunta di un lengthmetodo di sostituzione ImmutableStringpuò essere eseguita aggiungendo un lengthOverrideMethod:

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

Quindi, il puntatore a funzione per il lengthmetodo nel costruttore viene collegato a 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;
}

Ora, anziché avere un comportamento identico per il lengthmetodo in ImmutableStringclasse come la Stringclasse, ora il lengthmetodo farà riferimento al comportamento definito nella lengthOverrideMethodfunzione.

Devo aggiungere una dichiarazione di non responsabilità che sto ancora imparando a scrivere con uno stile di programmazione orientato agli oggetti in C, quindi probabilmente ci sono punti che non ho spiegato bene, o che potrebbero non essere chiari in termini di come implementare al meglio OOP in C. Ma il mio scopo era quello di provare a illustrare uno dei molti usi dei puntatori a funzione.

Per ulteriori informazioni su come eseguire la programmazione orientata agli oggetti in C, fare riferimento alle seguenti domande:


22
Questa risposta è orribile! Non solo implica che OO in qualche modo dipende dalla notazione dei punti, ma incoraggia anche a mettere spazzatura nei tuoi oggetti!
Alexei Averchenko,

27
Questo è OO tutto bene, ma non ovunque vicino all'OO in stile C. Quello che hai implementato in modo errato è OO basato su prototipo in stile Javascript. Per ottenere OO in stile C ++ / Pascal, è necessario: 1. Avere una const const per una tabella virtuale di ogni classe con membri virtuali. 2. Puntare su quella struttura negli oggetti polimorfici. 3. Chiamare i metodi virtuali tramite la tabella virtuale e tutti gli altri metodi direttamente, in genere attenendosi ad alcune ClassName_methodNameconvenzioni di denominazione delle funzioni. Solo allora otterrai gli stessi costi di runtime e archiviazione di quelli in C ++ e Pascal.
Ripristina Monica il

19
Lavorare con OO con un linguaggio che non è destinato a essere OO è sempre una cattiva idea. Se vuoi OO e hai ancora C, lavora con C ++.
rbaleksandar,

20
@rbaleksandar Dillo agli sviluppatori del kernel Linux. "Sempre una cattiva idea" è rigorosamente la tua opinione, con la quale non sono assolutamente d'accordo.
Jonathon Reinhart,

6
Mi piace questa risposta ma non lancio malloc
cat

227

La guida per essere licenziato: Come abusare dei puntatori a funzioni in GCC su macchine x86 compilando il codice a mano:

Questi valori letterali di stringa sono byte di codice macchina x86 a 32 bit. 0xC3è un sistema x86 retdi istruzioni .

Normalmente non li scriveresti a mano, scriveresti in un linguaggio assembly e poi utilizzeresti un assemblatore come nasmper assemblarlo in un binario piatto che eseguiresti in una stringa in C letterale.

  1. Restituisce il valore corrente sul registro EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Scrivi una funzione di scambio

    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. Scrivi un contatore for-loop su 1000, chiamando ogni volta una funzione

    ((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. Puoi persino scrivere una funzione ricorsiva che conta fino a 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);

Si noti che i compilatori inseriscono valori letterali di stringa nella .rodatasezione (o .rdatasu Windows), che è collegata come parte del segmento di testo (insieme al codice per le funzioni).

Il segmento di testo ha l'autorizzazione Read + Exec, quindi il cast dei letterali stringa per i puntatori di funzioni funziona senza necessità mprotect()o VirtualProtect()chiamate di sistema come quelle necessarie per la memoria allocata dinamicamente. (O gcc -z execstackcollega il programma con stack + segmento dati + eseguibile heap, come un trucco rapido.)


Per disassemblarli, è possibile compilarlo per mettere un'etichetta sui byte e utilizzare un disassemblatore.

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

Compilando gcc -c -m32 foo.ce disassemblando objdump -D -rwC -Mintel, possiamo ottenere l'assemblaggio e scoprire che questo codice viola l'ABI bloccando EBX (un registro protetto dalle chiamate) ed è generalmente inefficiente.

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

Questo codice macchina funzionerà (probabilmente) in codice a 32 bit su Windows, Linux, OS X e così via: le convenzioni di chiamata predefinite su tutti quei sistemi operativi passano gli argomenti nello stack anziché in modo più efficiente nei registri. Ma EBX è preservato dalle chiamate in tutte le normali convenzioni di chiamata, quindi usarlo come un registro scratch senza salvarlo / ripristinarlo può facilmente causare l'arresto anomalo del chiamante.


8
Nota: questo non funziona se la prevenzione dell'esecuzione dei dati è abilitata (ad es. Su Windows XP SP2 +), poiché le stringhe C non sono normalmente contrassegnate come eseguibili.
SicurezzaMatt

5
Ciao Matt! A seconda del livello di ottimizzazione, GCC spesso incorporerà le costanti di stringa nel segmento TEXT, quindi funzionerà anche con la versione più recente di Windows a condizione che non si impedisca questo tipo di ottimizzazione. (IIRC, la versione MINGW al momento del mio post più di due anni fa incorpora valori letterali di stringa al livello di ottimizzazione predefinito)
Lee

10
qualcuno potrebbe spiegare cosa sta succedendo qui? Cosa sono quei letterali stringa dall'aspetto strano?
Aj

56
@ajay Sembra che stia scrivendo valori esadecimali non elaborati (ad esempio '\ x00' è uguale a '/ 0', sono entrambi uguali a 0) in una stringa, quindi esegue il cast della stringa in un puntatore a una funzione C, quindi esegue il puntatore alla funzione C perché è il diavolo.
ejk314,

3
ciao FUZxxl, penso che potrebbe variare in base al compilatore e alla versione del sistema operativo. Il codice sopra sembra funzionare bene su codepad.org; codepad.org/FMSDQ3ME
Lee

115

Uno dei miei usi preferiti per i puntatori a funzioni è l'iteratore semplice e economico:

#include <stdio.h>
#define MAX_COLORS  256

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

Color Colors[MAX_COLORS];


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

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

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

    eachColor(printColor);
}

7
Dovresti anche passare un puntatore ai dati specificati dall'utente se vuoi in qualche modo estrarre qualsiasi output dalle iterazioni (pensa alle chiusure).
Alexei Averchenko,

1
Concordato. Tutti i miei iteratori simile a questa: int (*cb)(void *arg, ...). Il valore di ritorno dell'iteratore mi consente anche di interrompere in anticipo (se diverso da zero).
Jonathon Reinhart,

24

I puntatori a funzione diventano facili da dichiarare una volta che hai i dichiaratori di base:

  • id: ID: ID è un
  • Pointer: *D : puntatore D
  • Funzione: D(<parameters>): D funzione taking <parametri >di ritorno

Mentre D è un altro dichiaratore creato usando quelle stesse regole. Alla fine, da qualche parte, finisce conID (vedi sotto per un esempio), che è il nome dell'entità dichiarata. Proviamo a costruire una funzione prendendo un puntatore a una funzione che non prende nulla e restituisce int e restituisce un puntatore a una funzione che prende un carattere e restituisce int. Con i def di tipo è così

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

Come vedi, è abbastanza facile costruirlo usando typedefs. Senza typedefs, non è nemmeno difficile con le regole del dichiaratore sopra, applicate in modo coerente. Come vedi ho perso la parte a cui punta il puntatore e la cosa che la funzione ritorna. Questo è ciò che appare alla sinistra della dichiarazione, e non è di interesse: viene aggiunto alla fine se si è già creato il dichiarante. Facciamolo. Costruendolo in modo coerente, prima parola - mostrando la struttura usando[ e ]:

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

Come vedi, uno può descrivere un tipo completamente aggiungendo i dichiaranti uno dopo l'altro. La costruzione può essere eseguita in due modi. Uno è dal basso verso l'alto, a partire dalla cosa giusta (foglie) e procedendo fino all'identificatore. L'altro modo è dall'alto verso il basso, a partire dall'identificatore, scendendo fino alle foglie. Mostrerò in entrambi i modi.

Dal basso verso l'alto

La costruzione inizia con la cosa a destra: la cosa restituita, che è la funzione che prende i caratteri. Per mantenere distinti i dichiaranti, li numererò:

D1(char);

Inserito direttamente il parametro char, poiché è banale. Aggiunta di un puntatore al dichiaratore sostituendo D1con *D2. Nota che dobbiamo racchiudere le parentesi *D2. Questo può essere conosciuto cercando la precedenza *-operatordell'operatore di chiamata di funzione e (). Senza le nostre parentesi, il compilatore lo leggerebbe come *(D2(char p)). Ma questo non sarebbe più un semplice rimpiazzo di D1 *D2, ovviamente. Le parentesi sono sempre consentite intorno ai dichiaratori. Quindi non fai nulla di sbagliato se ne aggiungi troppo, in realtà.

(*D2)(char);

Il tipo restituito è completo! Ora, sostituiamo D2con la funzione dichiaratore di funzione che prende <parameters>return , che è quello D3(<parameters>)che siamo ora.

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

Si noti che non sono necessarie parentesi, poiché questa volta vogliamo D3 essere dichiaratori di funzioni e non dichiaratori di puntatori. Fantastico, l'unica cosa che rimane sono i parametri per questo. Il parametro è fatto esattamente come il tipo restituito, solo con charsostituito da void. Quindi lo copierò:

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

Ho sostituito D2da ID1, dato che abbiamo finito con quel parametro (è già un puntatore a una funzione - non c'è bisogno di un altro dichiaratore). ID1sarà il nome del parametro. Ora, ho detto sopra alla fine, si aggiunge il tipo che tutti quei dichiaranti modificano - quello che appare alla sinistra di ogni dichiarazione. Per le funzioni, questo diventa il tipo restituito. Per i puntatori il tipo appuntito ecc ... È interessante quando si scrive il tipo, apparirà nell'ordine opposto, proprio a destra :) Comunque, sostituendolo si ottiene la dichiarazione completa. intNaturalmente entrambe le volte .

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

Ho chiamato l'identificatore della funzione ID0in quell'esempio.

Dall'alto al basso

Questo inizia dall'identificatore all'estrema sinistra nella descrizione del tipo, avvolgendo quel dichiaratore mentre percorriamo la strada a destra. Inizia con la funzione taking <parametri >di ritorno

ID0(<parameters>)

La cosa successiva nella descrizione (dopo "ritorno") era puntatore a . Incorporiamolo:

*ID0(<parameters>)

Poi la prossima cosa era functon prendere <i parametri >di ritorno . Il parametro è un semplice carattere, quindi lo inseriamo subito, poiché è davvero banale.

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

Nota le parentesi che abbiamo aggiunto, dal momento che vogliamo di nuovo che i *vincoli prima e poi i (char). Altrimenti leggerebbe la funzione prendendo i <parametri che >restituiscono la funzione ... . No, le funzioni che restituiscono funzioni non sono nemmeno consentite.

Ora dobbiamo solo mettere i <parametri >. Mostrerò una versione breve della deriveration, dal momento che penso che tu abbia già l'idea di come farlo.

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

Metti intdavanti ai dichiaranti come abbiamo fatto con il bottom-up, e abbiamo finito

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

La cosa carina

Il bottom-up o il top-down sono migliori? Sono abituato al bottom-up, ma alcune persone potrebbero sentirsi più a proprio agio con il top-down. Penso che sia una questione di gusti. Per inciso, se si applicano tutti gli operatori in quella dichiarazione, si otterrà un int:

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

Questa è una bella proprietà delle dichiarazioni in C: La dichiarazione afferma che se quegli operatori sono usati in un'espressione usando l'identificatore, allora produce il tipo a sinistra. È così anche per gli array.

Spero ti sia piaciuto questo piccolo tutorial! Ora possiamo collegarci a questo quando le persone si chiedono la strana sintassi delle dichiarazioni. Ho provato a mettere il minor numero di interni C possibile. Sentiti libero di modificare / riparare le cose in esso.


24

Un altro buon uso dei puntatori a funzioni:
passaggio indolore tra le versioni

Sono molto utili da utilizzare quando si desidera funzioni diverse in momenti diversi o fasi di sviluppo diverse. Ad esempio, sto sviluppando un'applicazione su un computer host che ha una console, ma la versione finale del software verrà inserita in un Avnet ZedBoard (che ha porte per display e console, ma non sono necessarie / ricercate per rilascio finale). Quindi, durante lo sviluppo, userò printfper visualizzare i messaggi di stato e di errore, ma quando avrò finito, non voglio stampare nulla. Ecco cosa ho fatto:

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

In version.cdefinirò i 2 prototipi di funzione presenti inversion.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;
}

Notare come il puntatore a funzione è prototipato in version.has

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

Quando viene fatto riferimento nell'applicazione, inizierà l'esecuzione ovunque sia puntato, che deve ancora essere definito.

In version.c, notare nella board_init()funzione in cui zprintfè assegnata una funzione univoca (la cui firma della funzione corrisponde) a seconda della versione definita inversion.h

zprintf = &printf; zprintf chiama printf per scopi di debug

o

zprintf = &noprint; zprintf ritorna e non esegue codice non necessario

L'esecuzione del codice sarà simile al seguente:

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

Il codice sopra verrà utilizzato printfse in modalità debug o non farà nulla se in modalità di rilascio. Questo è molto più semplice che passare attraverso l'intero progetto e commentare o eliminare il codice. Tutto quello che devo fare è cambiare la versione version.he il codice farà il resto!


4
Stai per perdere molto tempo durante le esibizioni. Invece potresti usare una macro che abilita e disabilita una sezione di codice basata su Debug / Release.
AlphaGoku,

19

Il puntatore a funzione è generalmente definito da typedef e utilizzato come valore param e return.

Le risposte sopra hanno già spiegato molto, faccio solo un esempio completo:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

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

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

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

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

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

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

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

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

    return 0;
}

14

Uno dei grandi usi per i puntatori a funzione in C è chiamare una funzione selezionata in fase di esecuzione. Ad esempio, la libreria di runtime C ha due routine qsortebsearch , che portano un puntatore a una funzione chiamata per confrontare due elementi ordinati; questo ti consente di ordinare o cercare, rispettivamente, qualsiasi cosa, in base ai criteri che desideri utilizzare.

Un esempio molto semplice, se esiste una funzione chiamata print(int x, int y)che a sua volta potrebbe richiedere di chiamare una funzione (o add()o sub(), che sono dello stesso tipo), allora cosa faremo, aggiungeremo un argomento puntatore alla print()funzione come mostrato di seguito :

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

L'output è:

il valore è: 410 il
valore è: 390


10

La funzione di avvio da zero ha alcuni indirizzi di memoria da cui iniziano l'esecuzione. Nel linguaggio assembly vengono chiamati come (chiama "indirizzo di memoria della funzione"). Ora ritorna a C Se la funzione ha un indirizzo di memoria, possono essere manipolati da Puntatori in C.So Secondo le regole di C

1.Prima devi dichiarare un puntatore alla funzione 2. Passa l'indirizzo della funzione desiderata

**** Nota-> le funzioni dovrebbero essere dello stesso tipo ****

Questo semplice programma illustrerà ogni cosa.

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

inserisci qui la descrizione dell'immagineDopodiché consente di vedere In che modo la macchina comprende Them.Glimpse dell'istruzione della macchina del programma sopra nell'architettura a 32 bit.

L'area con il segno rosso mostra come l'indirizzo viene scambiato e memorizzato in eax. Quindi la loro è un'istruzione call su eax. eax contiene l'indirizzo desiderato della funzione.


8

Un puntatore a funzione è una variabile che contiene l'indirizzo di una funzione. Poiché si tratta di una variabile puntatore, sebbene con alcune proprietà limitate, è possibile utilizzarlo praticamente come qualsiasi altra variabile puntatore nelle strutture dati.

L'unica eccezione che mi viene in mente è quella di considerare il puntatore a funzione come un riferimento a qualcosa di diverso da un singolo valore. Fare l'aritmetica del puntatore incrementando o decrementando un puntatore a funzione o aggiungendo / sottraendo un offset a un puntatore a funzione non è in realtà alcuna utilità poiché un puntatore a funzione punta solo a una singola cosa, il punto di ingresso di una funzione.

La dimensione di una variabile del puntatore a funzione, il numero di byte occupati dalla variabile, può variare a seconda dell'architettura sottostante, ad esempio x32 o x64 o altro.

La dichiarazione per una variabile di puntatore a funzione deve specificare lo stesso tipo di informazioni di una dichiarazione di funzione affinché il compilatore C esegua i tipi di controlli che esegue normalmente. Se non si specifica un elenco di parametri nella dichiarazione / definizione del puntatore a funzione, il compilatore C non sarà in grado di verificare l'uso dei parametri. Ci sono casi in cui questa mancanza di controllo può essere utile, ma ricorda solo che è stata rimossa una rete di sicurezza.

Qualche esempio:

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.

Le prime due dichiarazioni sono in qualche modo simili in quanto:

  • func è una funzione che accetta un int e a char *e restituisce anint
  • pFunc è un puntatore a cui è assegnato l'indirizzo di una funzione che accetta un int e a char *e restituisce anint

Quindi da quanto sopra potremmo avere una linea sorgente in cui l'indirizzo della funzione func() è assegnato alla variabile del puntatore a funzione pFunccome in pFunc = func;.

Notare la sintassi utilizzata con una dichiarazione / definizione del puntatore a funzione in cui le parentesi vengono utilizzate per superare le regole di precedenza dell'operatore naturale.

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

Diversi esempi di utilizzo

Alcuni esempi di utilizzo di un puntatore a funzione:

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

È possibile utilizzare elenchi di parametri a lunghezza variabile nella definizione di un puntatore a funzione.

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

Oppure non è possibile specificare un elenco di parametri. Questo può essere utile ma elimina la possibilità per il compilatore C di eseguire controlli sull'elenco degli argomenti fornito.

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

Calchi in stile C.

È possibile utilizzare i cast in stile C con i puntatori a funzione. Tuttavia, tieni presente che un compilatore C potrebbe essere in ritardo sui controlli o fornire avvisi anziché errori.

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.

Confronta il puntatore a funzione con l'uguaglianza

È possibile verificare che un puntatore a funzione sia uguale a un indirizzo di funzione particolare usando ifun'istruzione sebbene non sia sicuro di quanto utile sarebbe. Altri operatori di confronto sembrerebbero avere ancora meno 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;
}

Una matrice di puntatori di funzioni

E se vuoi avere una matrice di puntatori a funzione ciascuno degli elementi di cui l'elenco degli argomenti presenta differenze, puoi definire un puntatore a funzione con l'elenco degli argomenti non specificato (non voidche significa nessun argomento ma solo non specificato) qualcosa come il seguente sebbene tu potrebbe visualizzare avvisi dal compilatore C. Questo funziona anche per un parametro del puntatore a una funzione:

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.
}

Stile C namespaceUtilizzo di Globalstruct con puntatori a funzione

Puoi usare il static parola chiave per specificare una funzione il cui nome è ambito del file e quindi assegnarla a una variabile globale come modo per fornire qualcosa di simile alla namespacefunzionalità di C ++.

In un file di intestazione definire una struttura che sarà il nostro spazio dei nomi insieme a una variabile globale che lo utilizza.

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;

Quindi nel file sorgente 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};

Questo verrebbe quindi usato specificando il nome completo della variabile strutturale globale e il nome del membro per accedere alla funzione. Il constmodificatore viene utilizzato a livello globale in modo che non possa essere modificato accidentalmente.

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

Aree di applicazione degli indicatori di funzione

Un componente di libreria DLL potrebbe fare qualcosa di simile namespaceall'approccio in stile C in cui una particolare interfaccia di libreria è richiesta da un metodo factory in un'interfaccia di libreria che supporta la creazione di structpuntatori di funzioni contenenti .. Questa interfaccia di libreria carica la versione DLL richiesta, crea una struttura con i puntatori di funzione necessari, quindi restituisce la struttura al chiamante richiedente per l'uso.

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

e questo potrebbe essere usato come in:

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

Lo stesso approccio può essere utilizzato per definire un livello hardware astratto per il codice che utilizza un modello particolare dell'hardware sottostante. I puntatori a funzione sono compilati con funzioni specifiche dell'hardware da una fabbrica per fornire la funzionalità specifica dell'hardware che implementa le funzioni specificate nel modello hardware astratto. Questo può essere utilizzato per fornire un livello hardware astratto utilizzato dal software che chiama una funzione di fabbrica al fine di ottenere l'interfaccia di funzione hardware specifica, quindi utilizza i puntatori di funzione forniti per eseguire azioni per l'hardware sottostante senza bisogno di conoscere i dettagli di implementazione sulla destinazione specifica .

Puntatori a funzione per creare delegati, gestori e callback

È possibile utilizzare i puntatori a funzione come modo per delegare alcune attività o funzionalità. L'esempio classico in C è il puntatore alla funzione di delega di confronto utilizzato con le funzioni della libreria C standard qsort()ebsearch() per fornire l'ordine di confronto per ordinare un elenco di elementi o eseguire una ricerca binaria su un elenco ordinato di elementi. Il delegato della funzione di confronto specifica l'algoritmo di confronto utilizzato nell'ordinamento o nella ricerca binaria.

Un altro uso è simile all'applicazione di un algoritmo a un contenitore Libreria di modelli standard C ++.

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

    return pArray;
}

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

    return 1;
}

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

    return pArray;
}

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

    return 1;
}

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

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

Un altro esempio è con il codice sorgente della GUI in cui viene registrato un gestore per un particolare evento fornendo un puntatore a funzione che viene effettivamente chiamato quando si verifica l'evento. Il framework Microsoft MFC con le sue mappe dei messaggi utilizza qualcosa di simile per gestire i messaggi di Windows che vengono recapitati a una finestra o thread.

Le funzioni asincrone che richiedono un callback sono simili a un gestore eventi. L'utente della funzione asincrona chiama la funzione asincrona per avviare un'azione e fornisce un puntatore a funzione che la funzione asincrona chiamerà una volta completata l'azione. In questo caso l'evento è la funzione asincrona che completa il suo compito.


0

Poiché i puntatori a funzione sono spesso callback tipizzati, si consiglia di dare un'occhiata a callback sicuri . Lo stesso vale per i punti di ingresso, ecc. Di funzioni che non sono richiamate.

C è piuttosto volubile e perdona allo stesso tempo :)

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.