Come funzionano i puntatori di funzione in C?




6 Answers

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 -> e la mancanza di un new operatore è una donazione morta, ma di certo sembra implicare che stiamo impostando il testo di qualche classe String come "hello" .

Usando i puntatori di funzione, è possibile emulare i metodi in C.

Come è stato realizzato?

La classe String è in realtà una struct con un gruppo di puntatori a funzione che agiscono come un modo per simulare i metodi. Di seguito è riportata una dichiarazione parziale della 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();

Come si può vedere, i metodi della classe String sono in realtà dei puntatori alla funzione dichiarata. Nella preparazione dell'istanza della String , viene chiamata la funzione newString 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 funzione getString chiamata invocando il metodo get è definita come segue:

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

Una cosa che può essere notata è che non esiste un concetto di un'istanza di un oggetto e di avere metodi che sono effettivamente una parte di un oggetto, quindi un "oggetto autonomo" deve essere passato in ogni invocazione. (E l' internal è solo una struct nascosta che è stata omessa dal codice che precede l'elenco - è un modo di nascondere le informazioni, ma ciò non è rilevante per i puntatori di funzione.)

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

Con questa piccola spiegazione che deve passare in un riferimento a te stesso di mezzo, passeremo alla parte successiva, che è l' ereditarietà in C.

Diciamo che vogliamo creare una sottoclasse di String , ad esempio ImmutableString . Per rendere la stringa immutabile, il metodo set non sarà accessibile, pur mantenendo l'accesso a get e length , e costringerà 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 di funzione. Questa volta, la dichiarazione per il metodo set non è presente, quindi non può essere chiamata in 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;
}

String.get String.length , i puntatori di funzione ai metodi get e length riferiscono effettivamente al metodo String.get e String.length , passando per la variabile base che è un oggetto String memorizzato internamente.

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

Possiamo continuare ulteriormente con il polimorfismo in C.

Se per esempio volessimo cambiare il comportamento del metodo length per restituire 0 tutto il tempo nella classe ImmutableString per qualche ragione, tutto ciò che dovrebbe essere fatto è:

  1. Aggiungi una funzione che servirà come metodo di length prevalente.
  2. Vai al "costruttore" e imposta il puntatore alla funzione sul metodo della length override.

L'aggiunta di un metodo di length sovrascritta in ImmutableString può essere eseguita aggiungendo un lengthOverrideMethod :

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

Quindi, il puntatore della funzione per il metodo length nel costruttore è 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 metodo length nella classe ImmutableString come la classe String , ora il metodo length si riferirà al comportamento definito nella funzione lengthOverrideMethod .

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 dei punti che non ho spiegato bene, o che potrebbero semplicemente essere off-mark in termini di come implementare OOP al meglio in C. Ma il mio scopo era cercare di illustrare uno dei molti usi dei puntatori di funzione.

Per ulteriori informazioni su come eseguire la programmazione orientata agli oggetti in C, consultare le seguenti domande:

c function-pointers

Ho avuto qualche esperienza ultimamente con i puntatori di funzione in C.

Quindi, continuando con la tradizione di rispondere alle tue domande, ho deciso di fare un piccolo riassunto delle basi stesse, per coloro che hanno bisogno di una veloce immersione nell'argomento.




Uno dei miei usi preferiti per i puntatori di funzione è come iteratori semplici e poco costosi -

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



Un altro buon uso per i puntatori di funzione:
Passaggio tra le versioni senza dolore

Sono molto utili da utilizzare quando si desiderano funzioni diverse in momenti diversi o in fasi di sviluppo diverse. Ad esempio, sto sviluppando un'applicazione su un computer host con una console, ma la versione finale del software verrà messa su Avnet ZedBoard (che ha porte per display e console, ma non sono necessarie / ricercate per il rilascio finale). Quindi durante lo sviluppo userò printf per visualizzare lo stato e i messaggi di errore, ma quando ho 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

Nella version.c i 2 prototipi di funzioni presenti in version.h

version.c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

Si noti come il puntatore della funzione è prototipato in version.h come

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

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

In version.c , si noti nella funzione board_init() dove a zprintf è assegnata una funzione unica (la cui firma di funzione corrisponde) a seconda della versione che è definita in version.h

zprintf = &printf; zprintf chiama printf a scopo di debug

o

zprintf = &noprint; zprintf restituisce e non eseguirà codice non necessario

L'esecuzione del codice sarà simile a questa:

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 utilizzerà printf se in modalità debug, o non fare nulla se in modalità di rilascio. Questo è molto più facile che passare attraverso l'intero progetto e commentare o eliminare il codice. Tutto quello che devo fare è cambiare la versione in version.h e il codice farà il resto!




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

Un esempio molto semplice, se esiste una funzione chiamata print (int x, int y) che a sua volta può richiedere di chiamare add () function o sub () che sono di tipi simili allora cosa faremo, aggiungeremo una funzione argomento puntatore alla funzione print () come mostrato di seguito: -

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is : %d", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}



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

L'unica eccezione a cui riesco a pensare è il fatto che il puntatore della funzione indichi qualcosa di diverso da un singolo valore. Fare aritmetica del puntatore incrementando o decrementando un puntatore a funzione o aggiungendo / sottraendo un offset a un puntatore a funzione non è in realtà di alcuna utilità in quanto un puntatore a funzione punta solo a una singola cosa, il punto di ingresso di una funzione.

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

La dichiarazione per una variabile di puntatore a funzione deve specificare lo stesso tipo di informazione di una dichiarazione di funzione in modo che il compilatore C esegua i tipi di controlli che normalmente esegue. Se non si specifica un elenco di parametri nella dichiarazione / definizione del puntatore della funzione, il compilatore C non sarà in grado di controllare 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 un char * e restituisce un int
  • pFunc è un puntatore a funzione a cui viene assegnato l'indirizzo di una funzione che accetta un int e un char * e restituisce un int

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

Si noti la sintassi utilizzata con una dichiarazione / definizione del puntatore di funzione in cui vengono utilizzate le parentesi per superare le naturali regole di precedenza degli operatori.

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 diversi

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 l'opportunità per il compilatore C di eseguire controlli sulla lista degli argomenti fornita.

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

C stile Casts

È possibile utilizzare i cast di stile C con i puntatori di funzione. Tuttavia, si tenga presente che un compilatore C potrebbe non essere a conoscenza dei controlli o fornire avvertenze piuttosto che 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 funzione su uguaglianza

È possibile verificare che un puntatore a funzione sia uguale a un indirizzo di una particolare funzione utilizzando un'istruzione if se non sono sicuro di quanto sarebbe utile. 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 funzione

E se vuoi avere una serie di puntatori di funzione, ognuno degli elementi di cui l'elenco degli argomenti ha delle differenze, puoi definire un puntatore di funzione con l'elenco degli argomenti non specificato (non voidche significa nessun argomento ma solo non specificato) qualcosa come la seguente anche se tu potrebbe vedere gli avvisi dal compilatore C. Questo funziona anche per un parametro del puntatore di funzione per 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 namespaceUso globale structcon puntatori di funzioni

È possibile utilizzare la staticparola chiave per specificare una funzione il cui nome è scope del file e quindi assegnarlo a una variabile globale come metodo per fornire qualcosa di simile alla namespacefunzionalità di C ++.

In un file di intestazione definiamo una struttura che sarà il nostro spazio dei nomi insieme a una variabile globale che la usa.

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 sarebbe quindi usato specificando il nome completo della variabile struct globale e il nome del membro per accedere alla funzione. Il constmodificatore viene utilizzato su tutto il mondo in modo che non possa essere modificato per sbaglio.

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

Aree applicative dei puntatori funzione

Un componente della libreria DLL potrebbe fare qualcosa di simile namespaceall'approccio in stile C in cui è richiesta una particolare interfaccia di libreria da un metodo factory in un'interfaccia di libreria che supporta la creazione di un structpuntatore a funzione contenente. Questa interfaccia libreria carica la versione richiesta DLL, crea una struttura con i necessari puntatori di funzione, 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 particolare modello dell'hardware sottostante. I puntatori di funzione sono compilati da una funzione specifica dell'hardware da una fabbrica per fornire la funzionalità specifica dell'hardware che implementa le funzioni specificate nel modello hardware astratto. Questo può essere usato per fornire un livello hardware astratto usato dal software che richiama una funzione di fabbrica per ottenere l'interfaccia di funzione hardware specifica, quindi utilizza i puntatori di funzione forniti per eseguire azioni per l'hardware sottostante senza dover conoscere i dettagli dell'implementazione sul target specifico .

Funzione Puntatori per creare delegati, gestori e callback

È possibile utilizzare i puntatori di funzione come metodo per delegare alcune attività o funzionalità. L'esempio classico di C è il puntatore alla funzione delegate di confronto utilizzato con le funzioni della libreria C standard qsort()e bsearch()per fornire l'ordine di confronto per l'ordinamento di un elenco di elementi o l'esecuzione di una ricerca binaria su un elenco ordinato di elementi. La funzione di confronto delegato specifica l'algoritmo di confronto utilizzato nell'ordinamento o nella ricerca binaria.

Un altro uso è simile all'applicazione di un algoritmo a un contenitore di libreria modello 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 un gestore per un particolare evento è registrato 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 consegnati a una finestra o thread.

Le funzioni asincrone che richiedono una richiamata sono simili a un gestore di 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.




i puntatori di funzione sono utili in molte situazioni, ad esempio:

  • I membri degli oggetti COM sono puntatori alla funzione ag: This->lpVtbl->AddRef(This);AddRef è un puntatore a una funzione.
  • funzione callback, ad esempio una funzione definita dall'utente per confrontare due variabili da passare come callback a una funzione di ordinamento speciale.
  • molto utile per l'implementazione del plugin e l'SDK dell'applicazione.





Related