Come funzionano i puntatori di funzione in C?



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 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:

Question

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.




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



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



Poiché i puntatori di funzione sono spesso callback tipizzati, è possibile dare un'occhiata al tipo callback sicuri . Lo stesso vale per i punti di ingresso, ecc. Di funzioni che non sono callback.

C è abbastanza volubile e tollerante allo stesso tempo :)




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

  • I membri degli oggetti COM sono puntatori a function 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