Comprensione dei typedef per i puntatori a funzione in C


237

Sono sempre stato un po 'sconcertato quando leggevo il codice di altre persone che aveva battute dattiloscritte per puntatori a funzioni con argomenti. Ricordo che mi ci volle un po 'per arrivare a una simile definizione mentre cercavo di capire un algoritmo numerico scritto in C qualche tempo fa. Quindi, potresti condividere i tuoi suggerimenti e pensieri su come scrivere buoni dattiloscritti per i puntatori alle funzioni (Do e Do), perché sono utili e come capire il lavoro degli altri? Grazie!


1
Potete fornire alcuni esempi?
Artelius,

2
Non intendi typedef per i puntatori a funzione, anziché macro per i puntatori a funzione? Ho visto il primo ma non il secondo.
dave4420,

Risposte:


297

Considera la signal()funzione dello standard C:

extern void (*signal(int, void(*)(int)))(int);

Perfettamente oscuramente ovvio: è una funzione che accetta due argomenti, un numero intero e un puntatore a una funzione che accetta un numero intero come argomento e non restituisce nulla e signal()restituisce un puntatore a una funzione che accetta un numero intero come argomento e restituisce Niente.

Se scrivi:

typedef void (*SignalHandler)(int signum);

quindi puoi invece dichiarare signal()come:

extern  SignalHandler signal(int signum, SignalHandler handler);

Ciò significa la stessa cosa, ma di solito è considerato un po 'più facile da leggere. È più chiaro che la funzione accetta an inte a SignalHandlere restituisce a SignalHandler.

Ci vuole un po 'per abituarsi, però. L'unica cosa che non puoi fare, tuttavia, è scrivere una funzione del gestore del segnale usando SignalHandler typedefnella definizione della funzione.

Sono ancora della vecchia scuola che preferisce invocare un puntatore a funzione come:

(*functionpointer)(arg1, arg2, ...);

La sintassi moderna utilizza solo:

functionpointer(arg1, arg2, ...);

Posso capire perché funziona: preferisco solo sapere che devo cercare dove viene inizializzata la variabile piuttosto che per una funzione chiamata functionpointer.


Sam ha commentato:

Ho visto questa spiegazione prima. E poi, come è il caso ora, penso che ciò che non ho ottenuto fosse la connessione tra le due affermazioni:

    extern void (*signal(int, void()(int)))(int);  /*and*/

    typedef void (*SignalHandler)(int signum);
    extern SignalHandler signal(int signum, SignalHandler handler);

Oppure, quello che voglio chiedere è, qual è il concetto di base che si può usare per inventare la seconda versione che hai? Qual è il fondamentale che collega "SignalHandler" e il primo typedef? Penso che ciò che deve essere spiegato qui sia ciò che sta facendo typedef qui.

Proviamo di nuovo. Il primo di questi viene sollevato direttamente dallo standard C - l'ho riscritto e verificato che le parentesi fossero corrette (non fino a quando non l'ho corretto - è un biscotto difficile da ricordare).

Prima di tutto, ricorda che typedefintroduce un alias per un tipo. Quindi, l'alias è SignalHandlere il suo tipo è:

un puntatore a una funzione che accetta un numero intero come argomento e non restituisce nulla.

La parte "non restituisce nulla" è scritta void; l'argomento che è un numero intero è (credo) autoesplicativo. La seguente notazione è semplicemente (o no) il modo in cui C fa il sillabatore puntando alla funzione prendendo argomenti come specificato e restituendo il tipo dato:

type (*function)(argtypes);

Dopo aver creato il tipo di gestore del segnale, posso usarlo per dichiarare le variabili e così via. Per esempio:

static void alarm_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d)\n", __func__, signum);
}

static void signal_catcher(int signum)
{
    fprintf(stderr, "%s() called (%d) - exiting\n", __func__, signum);
    exit(1);
}

static struct Handlers
{
    int              signum;
    SignalHandler    handler;
} handler[] =
{
    { SIGALRM,   alarm_catcher  },
    { SIGINT,    signal_catcher },
    { SIGQUIT,   signal_catcher },
};

int main(void)
{
    size_t num_handlers = sizeof(handler) / sizeof(handler[0]);
    size_t i;

    for (i = 0; i < num_handlers; i++)
    {
        SignalHandler old_handler = signal(handler[i].signum, SIG_IGN);
        if (old_handler != SIG_IGN)
            old_handler = signal(handler[i].signum, handler[i].handler);
        assert(old_handler == SIG_IGN);
    }

    ...continue with ordinary processing...

    return(EXIT_SUCCESS);
}

Nota Come evitare l'uso printf()in un gestore di segnale?

Quindi, cosa abbiamo fatto qui - a parte omettere 4 intestazioni standard che sarebbero necessarie per compilare il codice in modo pulito?

Le prime due funzioni sono funzioni che accettano un singolo numero intero e non restituiscono nulla. Uno di loro in realtà non ritorna affatto grazie al exit(1);ma l'altro ritorna dopo aver stampato un messaggio. Essere consapevoli del fatto che lo standard C non consente di fare molto all'interno di un gestore di segnale; POSIX è un po 'più generoso in ciò che è permesso, ma ufficialmente non sanziona le chiamate fprintf(). Stampo anche il numero del segnale ricevuto. Nella alarm_handler()funzione, il valore sarà sempre SIGALRMdato che è l'unico segnale per cui è un gestore, ma signal_handler()potrebbe ottenere SIGINTo SIGQUITcome numero di segnale perché la stessa funzione viene utilizzata per entrambi.

Quindi creo una matrice di strutture, in cui ogni elemento identifica un numero di segnale e il gestore da installare per quel segnale. Ho scelto di preoccuparmi di 3 segnali; Mi preoccuperei spesso SIGHUP, SIGPIPEe SIGTERManche e se fossero definiti ( #ifdefcompilazione condizionale), ma ciò complica semplicemente le cose. Probabilmente userei POSIX sigaction()invece di signal(), ma questo è un altro problema; rimaniamo con ciò che abbiamo iniziato.

La main()funzione scorre sull'elenco dei gestori da installare. Per ogni gestore, prima chiama signal()per scoprire se il processo sta attualmente ignorando il segnale e, mentre lo fa, si installa SIG_IGNcome gestore, il che assicura che il segnale rimanga ignorato. Se in precedenza il segnale non veniva ignorato, viene richiamato signal(), questa volta per installare il gestore di segnale preferito. (L'altro valore è presumibilmente SIG_DFL, il gestore del segnale predefinito per il segnale.) Poiché la prima chiamata a "signal ()" imposta il gestore su SIG_IGNe signal()restituisce il precedente gestore degli errori, il valore di oldafter ifdell'istruzione deve essere SIG_IGN- da qui l'asserzione. (Beh, potrebbe essereSIG_ERR se qualcosa fosse andato storto in modo drammatico, ma poi l'avrei appreso dall'asserzione.)

Il programma quindi fa le sue cose ed esce normalmente.

Si noti che il nome di una funzione può essere considerato come un puntatore a una funzione del tipo appropriato. Quando non si applicano le parentesi di chiamata di funzione, come ad esempio negli inizializzatori, il nome della funzione diventa un puntatore a funzione. Questo è anche il motivo per cui è ragionevole invocare funzioni tramite la pointertofunction(arg1, arg2)notazione; quando vedi alarm_handler(1), puoi considerare che alarm_handlerè un puntatore alla funzione e quindi alarm_handler(1)è un richiamo di una funzione tramite un puntatore a funzione.

Quindi, finora, ho dimostrato che una SignalHandlervariabile è relativamente semplice da usare, purché tu abbia del giusto tipo di valore da assegnare ad essa - che è ciò che forniscono le due funzioni del gestore del segnale.

Ora torniamo alla domanda: come si relazionano le due dichiarazioni signal().

Rivediamo la seconda dichiarazione:

 extern SignalHandler signal(int signum, SignalHandler handler);

Se abbiamo cambiato il nome della funzione e il tipo in questo modo:

 extern double function(int num1, double num2);

non avresti problemi a interpretare questo come una funzione che accetta un inte un doublecome argomenti e restituisce un doublevalore (vuoi? forse faresti meglio a non preoccuparti se questo è problematico - ma forse dovresti essere cauto nel porre domande difficili come questo se è un problema).

Ora, invece di essere a double, la signal()funzione prende a SignalHandlercome secondo argomento e ne restituisce uno come risultato.

La meccanica con cui può anche essere trattata come:

extern void (*signal(int signum, void(*handler)(int signum)))(int signum);

sono difficili da spiegare - quindi probabilmente lo rovinerò. Questa volta ho dato i nomi dei parametri, sebbene i nomi non siano critici.

In generale, in C, il meccanismo di dichiarazione è tale che se si scrive:

type var;

quindi quando scrivi varrappresenta un valore del dato type. Per esempio:

int     i;            // i is an int
int    *ip;           // *ip is an int, so ip is a pointer to an integer
int     abs(int val); // abs(-1) is an int, so abs is a (pointer to a)
                      // function returning an int and taking an int argument

Nello standard, typedefviene considerato come una classe di memoria nella grammatica, piuttosto come statice externsono classi di memoria.

typedef void (*SignalHandler)(int signum);

significa che quando vedi una variabile di tipo SignalHandler(diciamo alarm_handler) invocata come:

(*alarm_handler)(-1);

il risultato ha type void- non c'è risultato. Ed (*alarm_handler)(-1);è un'invocazione di alarm_handler()con argomento -1.

Quindi, se dichiarassimo:

extern SignalHandler alt_signal(void);

significa che:

(*alt_signal)();

rappresenta un valore vuoto. E quindi:

extern void (*alt_signal(void))(int signum);

è equivalente. Ora signal()è più complesso perché non solo restituisce a SignalHandler, accetta anche SignalHandlerargomenti sia int che a as:

extern void (*signal(int signum, SignalHandler handler))(int signum);

extern void (*signal(int signum, void (*handler)(int signum)))(int signum);

Se questo ti confonde ancora, non sono sicuro di come aiutarti - è ancora ad alcuni livelli misterioso per me, ma mi sono abituato a come funziona e quindi posso dirti che se rimani con esso per altri 25 anni o giù di lì, diventerà una seconda natura per te (e forse anche un po 'più veloce se sei intelligente).


3
Ho visto questa spiegazione prima. E poi, come è il caso ora, penso che ciò che non ho ottenuto fosse la connessione tra le due affermazioni: extern void ( signal (int, void ( ) (int))) (int); / * e * / typedef void (* SignalHandler) (int signum); segnale SignalHandler esterno (int signum, gestore SignalHandler); Oppure, quello che voglio chiedere è, qual è il concetto di base che si può usare per inventare la seconda versione che hai? Qual è il fondamentale che collega "SignalHandler" e il primo typedef? Penso che ciò che deve essere spiegato qui sia ciò che sta facendo typedef qui. Thx

6
Ottima risposta, sono contento di essere tornato a questo thread. Non credo di aver capito tutto, ma un giorno lo farò. Questo è il motivo per cui mi piace SO. Grazie.
toto

2
Solo per scegliere un nit: non è sicuro chiamare printf () e gli amici all'interno di un gestore di segnale; printf () non rientra (fondamentalmente perché può chiamare malloc (), che non è
rientrato

4
Ciò extern void (*signal(int, void(*)(int)))(int);significa che la signal(int, void(*)(int))funzione restituirà un puntatore a void f(int). Quando si desidera specificare un puntatore a funzione come valore di ritorno , la sintassi diventa complicata. Devi posizionare il tipo di valore restituito a sinistra e l'elenco degli argomenti a destra , mentre è il centro che stai definendo. E in questo caso, la signal()stessa funzione prende un puntatore a funzione come parametro, il che complica ancora di più le cose. La buona notizia è che, se riesci a leggere questo, la Forza è già con te. :).
smwikipedia,

1
Qual è la vecchia scuola sull'uso &di fronte a un nome di funzione? È totalmente inutile; inutile, anche. E sicuramente non "vecchia scuola". La vecchia scuola usa un nome di funzione chiaro e semplice.
Jonathan Leffler,

80

Un puntatore a funzione è come qualsiasi altro puntatore, ma punta all'indirizzo di una funzione anziché all'indirizzo di dati (su heap o stack). Come ogni puntatore, deve essere digitato correttamente. Le funzioni sono definite dal loro valore di ritorno e dai tipi di parametri che accettano. Pertanto, al fine di descrivere completamente una funzione, è necessario includerne il valore restituito e il tipo di ciascun parametro è accettato. Quando si digita una tale definizione, si assegna un "nome descrittivo" che semplifica la creazione e il riferimento a puntatori utilizzando tale definizione.

Ad esempio, supponiamo che tu abbia una funzione:

float doMultiplication (float num1, float num2 ) {
    return num1 * num2; }

quindi il seguente typedef:

typedef float(*pt2Func)(float, float);

può essere usato per indicare questa doMulitplicationfunzione. Sta semplicemente definendo un puntatore a una funzione che restituisce un float e accetta due parametri, ognuno di tipo float. Questa definizione ha il nome descrittivo pt2Func. Nota che pt2Funcpuò puntare a QUALSIASI funzione che restituisce un float e accetta 2 float.

Quindi puoi creare un puntatore che punta alla funzione doMultiplication come segue:

pt2Func *myFnPtr = &doMultiplication;

e puoi invocare la funzione usando questo puntatore come segue:

float result = (*myFnPtr)(2.0, 5.1);

Questo rende buona lettura: http://www.newty.de/fpt/index.html


grazie psicotik! Questo è stato utile. Il collegamento alla pagina Web dei puntatori a funzione è davvero utile. Leggendolo ora.

... Tuttavia, quel link newty.de non sembra parlare affatto dei typedef :( Quindi anche se quel link è fantastico, ma le risposte in questo thread sui typedefs sono inestimabili!

11
Potresti voler fare pt2Func myFnPtr = &doMultiplication;invece che pt2Func *myFnPtr = &doMultiplication;come myFnPtrè già un puntatore.
Tamilselvan,

1
dichiarando pt2Func * myFnPtr = & doMultiplication; invece di pt2Func myFnPtr = & doMultiplication; lancia un avvertimento.
AlphaGoku,

2
@Tamilselvan è corretto. myFunPtrè già un puntatore a funzione quindi usapt2Func myFnPtr = &doMultiplication;
Dustin Biser il

35

Un modo molto semplice per comprendere typedef del puntatore a funzione:

int add(int a, int b)
{
    return (a+b);
}

typedef int (*add_integer)(int, int); //declaration of function pointer

int main()
{
    add_integer addition = add; //typedef assigns a new variable i.e. "addition" to original function "add"
    int c = addition(11, 11);   //calling function via new variable
    printf("%d",c);
    return 0;
}

32

cdeclè un ottimo strumento per decifrare strane sintassi come dichiarazioni di puntatori a funzioni. Puoi usarlo anche per generarli.

Per quanto riguarda i suggerimenti per rendere le dichiarazioni complicate più facili da analizzare per la futura manutenzione (da soli o da altri), raccomando di creare typedefpiccoli pezzi e di usare quei piccoli pezzi come elementi costitutivi per espressioni più grandi e più complicate. Per esempio:

typedef int (*FUNC_TYPE_1)(void);
typedef double (*FUNC_TYPE_2)(void);
typedef FUNC_TYPE_1 (*FUNC_TYPE_3)(FUNC_TYPE_2);

piuttosto che:

typedef int (*(*FUNC_TYPE_3)(double (*)(void)))(void);

cdecl posso aiutarti con queste cose:

cdecl> explain int (*FUNC_TYPE_1)(void)
declare FUNC_TYPE_1 as pointer to function (void) returning int
cdecl> explain double (*FUNC_TYPE_2)(void)
declare FUNC_TYPE_2 as pointer to function (void) returning double
cdecl> declare FUNC_TYPE_3 as pointer to function (pointer to function (void) returning double) returning pointer to function (void) returning int
int (*(*FUNC_TYPE_3)(double (*)(void )))(void )

Ed è (in effetti) esattamente come ho generato quel pazzo pasticcio sopra.


2
Ciao Carl, quello è stato un esempio e una spiegazione molto perspicaci. Inoltre, grazie per aver mostrato l'uso di cdecl. Molto apprezzato.

C'è cdecl per windows?
Jack,

@Jack, sono sicuro che puoi costruirlo, sì.
Carl Norum,

2
C'è anche cdecl.org che offre lo stesso tipo di funzionalità ma online. Utile per noi sviluppatori Windows.
zaknotzach,

12
int add(int a, int b)
{
  return (a+b);
}
int minus(int a, int b)
{
  return (a-b);
}

typedef int (*math_func)(int, int); //declaration of function pointer

int main()
{
  math_func addition = add;  //typedef assigns a new variable i.e. "addition" to original function "add"
  math_func substract = minus; //typedef assigns a new variable i.e. "substract" to original function "minus"

  int c = addition(11, 11);   //calling function via new variable
  printf("%d\n",c);
  c = substract(11, 5);   //calling function via new variable
  printf("%d",c);
  return 0;
}

L'output di questo è:

22

6

Si noti che lo stesso definitore math_func è stato usato per dichiarare entrambe le funzioni.

Lo stesso approccio di typedef può essere usato per la struttura esterna (usando sturuct in altri file.)


5

Utilizzare typedefs per definire tipi più complicati, ad esempio i puntatori a funzione

Prenderò l'esempio della definizione di una macchina a stati in C

    typedef  int (*action_handler_t)(void *ctx, void *data);

ora abbiamo definito un tipo chiamato action_handler che accetta due puntatori e restituisce un int

definisce la tua macchina a stati

    typedef struct
    {
      state_t curr_state;   /* Enum for the Current state */
      event_t event;  /* Enum for the event */
      state_t next_state;   /* Enum for the next state */
      action_handler_t event_handler; /* Function-pointer to the action */

     }state_element;

Il puntatore a funzione dell'azione sembra un tipo semplice e typedef serve principalmente a questo scopo.

Tutti i gestori dei miei eventi ora dovrebbero aderire al tipo definito da action_handler

    int handle_event_a(void *fsm_ctx, void *in_msg );

    int handle_event_b(void *fsm_ctx, void *in_msg );

Riferimenti:

Programmazione Expert C di Linden


4

Questo è l'esempio più semplice di puntatori a funzione e array di puntatori a funzione che ho scritto come esercizio.

    typedef double (*pf)(double x);  /*this defines a type pf */

    double f1(double x) { return(x+x);}
    double f2(double x) { return(x*x);}

    pf pa[] = {f1, f2};


    main()
    {
        pf p;

        p = pa[0];
        printf("%f\n", p(3.0));
        p = pa[1];
        printf("%f\n", p(3.0));
    }
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.