wake-up-neo.com

Grundlegendes zu typedefs für Funktionszeiger in C

Ich war schon immer ein bisschen ratlos, als ich den Code anderer Leute las, der typedefs für Zeiger auf Funktionen mit Argumenten enthielt. Ich erinnere mich, dass ich eine Weile gebraucht habe, um zu einer solchen Definition zu gelangen, während ich versuchte, einen numerischen Algorithmus zu verstehen, der vor einiger Zeit in C geschrieben wurde. Könnten Sie uns also Ihre Tipps und Gedanken mitteilen, wie Sie gute Typendefinitionen für Zeiger auf Funktionen (Do's und Do's not's) schreiben, warum sie nützlich sind und wie Sie die Arbeit anderer verstehen? Vielen Dank!

219
user192712

Betrachten Sie die Funktion signal() aus dem C-Standard:

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

Völlig undurchsichtig - es ist eine Funktion, die zwei Argumente akzeptiert, eine Ganzzahl und einen Zeiger auf eine Funktion, die eine Ganzzahl als Argument akzeptiert und nichts zurückgibt, und es (signal()) gibt einen Zeiger auf eine Funktion zurück, die nimmt eine ganze Zahl als Argument und gibt nichts zurück.

Wenn du schreibst:

typedef void (*SignalHandler)(int signum);

dann können Sie stattdessen signal() deklarieren als:

extern  SignalHandler signal(int signum, SignalHandler handler);

Dies bedeutet dasselbe, wird jedoch normalerweise als etwas leichter lesbar angesehen. Es ist klarer, dass die Funktion ein int und ein SignalHandler akzeptiert und ein SignalHandler zurückgibt.

Es ist allerdings etwas gewöhnungsbedürftig. Das einzige, was Sie nicht tun können, ist eine Signalhandlerfunktion zu schreiben, indem Sie SignalHandlertypedef in der Funktionsdefinition verwenden.

Ich bin immer noch von der alten Schule, die es vorzieht, einen Funktionszeiger aufzurufen als:

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

Moderne Syntax verwendet nur:

functionpointer(arg1, arg2, ...);

Ich kann sehen, warum das funktioniert - ich möchte nur lieber wissen, dass ich nach dem Initialisierungsort der Variablen suchen muss, anstatt nach einer Funktion namens functionpointer.


Sam kommentierte:

Ich habe diese Erklärung schon einmal gesehen. Und dann, wie es jetzt der Fall ist, denke ich, was ich nicht verstanden habe, war die Verbindung zwischen den beiden Aussagen:

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

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

Oder, was ich fragen möchte, ist, was ist das zugrunde liegende Konzept, mit dem man die zweite Version entwickeln kann, die Sie haben? Was ist das Fundament, das "SignalHandler" und das erste typedef verbindet? Ich denke, was hier erklärt werden muss, ist das, was typedef hier tatsächlich tut.

Lass es uns erneut versuchen. Die erste davon wurde direkt aus dem C-Standard entfernt - ich habe sie erneut getippt und überprüft, ob die Klammern stimmen (erst, wenn ich sie korrigiert habe - es ist ein schwieriges Cookie, an das ich mich erinnern kann).

Denken Sie zunächst daran, dass typedef einen Alias ​​für einen Typ einführt. Der Alias ​​lautet also SignalHandler, und sein Typ ist:

ein Zeiger auf eine Funktion, die eine Ganzzahl als Argument verwendet und nichts zurückgibt.

Der Teil 'gibt nichts zurück' ist geschrieben void; Das Argument, das eine ganze Zahl ist, ist (ich vertraue darauf) selbsterklärend. Die folgende Notation beschreibt einfach (oder nicht), wie C einen Zeiger auf eine Funktion schreibt, die die angegebenen Argumente verwendet und den angegebenen Typ zurückgibt:

type (*function)(argtypes);

Nachdem ich den Signalhandler-Typ erstellt habe, kann ich ihn zum Deklarieren von Variablen usw. verwenden. Zum Beispiel:

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

Bitte beachten Sie Wie vermeide ich die Verwendung von printf() in einem Signalhandler?

Also, was haben wir hier gemacht - abgesehen davon, dass 4 Standard-Header weggelassen wurden, die benötigt würden, um den Code sauber kompilieren zu lassen?

Die ersten beiden Funktionen sind Funktionen, die eine einzelne Ganzzahl annehmen und nichts zurückgeben. Einer von ihnen kehrt dank der Funktion exit(1); überhaupt nicht zurück, der andere jedoch nach dem Drucken einer Nachricht. Beachten Sie, dass der C-Standard es Ihnen nicht erlaubt, sehr viel in einem Signalhandler zu tun. POSIX ist ein bisschen großzügiger, was erlaubt ist, sanktioniert aber offiziell nicht das Aufrufen von fprintf(). Ich drucke auch die empfangene Signalnummer aus. In der alarm_handler() -Funktion ist der Wert immer SIGALRM, da dies das einzige Signal ist, für das es ein Handler ist, aber signal_handler() kann SIGINT oder SIGQUIT als Signalnummer, da für beide die gleiche Funktion verwendet wird.

Dann erstelle ich ein Array von Strukturen, in denen jedes Element eine Signalnummer und den für dieses Signal zu installierenden Handler identifiziert. Ich habe beschlossen, mich um 3 Signale zu kümmern. Ich mache mir oft Sorgen um SIGHUP, SIGPIPE und SIGTERM und darüber, ob sie definiert sind (#ifdef Bedingte Kompilierung), aber das erschwert die Sache. Ich würde wahrscheinlich auch POSIX sigaction() anstelle von signal() verwenden, aber das ist ein anderes Problem. Bleiben wir bei dem, womit wir angefangen haben.

Die Funktion main() durchläuft die Liste der zu installierenden Handler. Für jeden Handler wird zuerst signal() aufgerufen, um herauszufinden, ob der Prozess das Signal derzeit ignoriert. Dabei wird SIG_IGN Als Handler installiert, wodurch sichergestellt wird, dass das Signal ignoriert bleibt. Wenn das Signal zuvor nicht ignoriert wurde, ruft es erneut signal() auf, um den bevorzugten Signalhandler zu installieren. (Der andere Wert ist vermutlich SIG_DFL, Der Standard-Signal-Handler für das Signal.) Da der erste Aufruf von 'signal ()' den Handler auf SIG_IGN Und signal() gesetzt hat Gibt den vorherigen Fehlerbehandler zurück. Der Wert von old nach der Anweisung if muss SIG_IGN lauten - daher die Behauptung. (Nun, es könnte SIG_ERR Sein, wenn etwas dramatisch schief geht - aber das erfahre ich dann aus dem Assert-Firing.)

Das Programm erledigt dann seine Aufgaben und wird normal beendet.

Beachten Sie, dass der Name einer Funktion als Zeiger auf eine Funktion des entsprechenden Typs angesehen werden kann. Wenn Sie die Klammern des Funktionsaufrufs nicht anwenden, wie beispielsweise in den Initialisierern, wird der Funktionsname zu einem Funktionszeiger. Aus diesem Grund ist es auch sinnvoll, Funktionen über die pointertofunction(arg1, arg2) -Notation aufzurufen. Wenn Sie alarm_handler(1) sehen, können Sie davon ausgehen, dass alarm_handler ein Zeiger auf die Funktion ist und daher alarm_handler(1) ein Aufruf einer Funktion über einen Funktionszeiger ist.

Bisher habe ich gezeigt, dass die Verwendung einer SignalHandler - Variablen relativ einfach ist, solange Sie die richtige Art von Wert zuweisen können - das ist es, was die beiden Signalhandler tun funktionen bieten.

Nun kehren wir zu der Frage zurück, in welcher Beziehung die beiden Deklarationen für signal() zueinander stehen.

Sehen wir uns die zweite Erklärung an:

 extern SignalHandler signal(int signum, SignalHandler handler);

Wenn wir den Funktionsnamen und den Typ wie folgt geändert haben:

 extern double function(int num1, double num2);

sie hätten kein Problem damit, dies als eine Funktion zu interpretieren, die ein int und ein double als Argumente verwendet und einen double - Wert zurückgibt. schätze, ob das problematisch ist - aber vielleicht solltest du vorsichtig sein, wenn du so schwere Fragen stellst wie diese, wenn es ein Problem ist).

Anstatt nun ein double zu sein, nimmt die signal() -Funktion ein SignalHandler als zweites Argument und gibt eines als Ergebnis zurück.

Die Mechanik, mit der das auch behandelt werden kann als:

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

sind schwierig zu erklären - also werde ich es wahrscheinlich vermasseln. Diesmal habe ich die Parameter benannt - obwohl die Namen nicht kritisch sind.

Im Allgemeinen ist der Deklarationsmechanismus in C so, dass wenn Sie schreiben:

type var;

wenn Sie dann var schreiben, repräsentiert dies einen Wert des gegebenen type. Zum Beispiel:

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

Im Standard wird typedef als Speicherklasse in der Grammatik behandelt, ähnlich wie static und extern Speicherklassen sind.

typedef void (*SignalHandler)(int signum);

bedeutet, dass, wenn Sie eine Variable vom Typ SignalHandler sehen (sagen wir alarm_handler), die aufgerufen wird als:

(*alarm_handler)(-1);

das Ergebnis hat type void - es gibt kein Ergebnis. Und (*alarm_handler)(-1); Ist ein Aufruf von alarm_handler() mit dem Argument -1.

Also, wenn wir erklärt haben:

extern SignalHandler alt_signal(void);

es bedeutet, dass:

(*alt_signal)();

repräsentiert einen ungültigen Wert. Und deshalb:

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

ist gleichwertig. Jetzt ist signal() komplexer, da es nicht nur ein SignalHandler zurückgibt, sondern auch ein int und ein SignalHandler als Argumente akzeptiert:

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

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

Wenn Sie das immer noch verwirrt, bin ich mir nicht sicher, wie Sie helfen sollen - es ist mir immer noch auf einer mysteriösen Ebene, aber ich habe mich an die Funktionsweise gewöhnt und kann Ihnen daher sagen, wenn Sie noch 25 Jahre dabei bleiben oder so, es wird für dich zur zweiten Natur (und vielleicht sogar ein bisschen schneller, wenn du schlau bist).

278

Ein Funktionszeiger ist wie jeder andere Zeiger, zeigt jedoch auf die Adresse einer Funktion anstelle der Adresse von Daten (auf Heap oder Stack). Wie jeder Zeiger muss er richtig eingegeben werden. Funktionen werden durch ihren Rückgabewert und die von ihnen akzeptierten Parametertypen definiert. Um eine Funktion vollständig zu beschreiben, müssen Sie ihren Rückgabewert angeben und den Typ jedes Parameters akzeptieren. Wenn Sie eine solche Definition eingeben, geben Sie ihr einen Anzeigenamen, der das Erstellen und Referenzieren von Zeigern mit dieser Definition erleichtert.

Nehmen wir zum Beispiel an, Sie haben eine Funktion:

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

dann die folgende typedef:

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

kann verwendet werden, um auf diese doMulitplication -Funktion zu verweisen. Es wird einfach ein Zeiger auf eine Funktion definiert, die einen float zurückgibt und zwei Parameter vom Typ float verwendet. Diese Definition hat den Anzeigenamen pt2Func. Beachten Sie, dass pt2Func kann auf JEDE Funktion zeigen, die ein float zurückgibt und 2 floats aufnimmt.

So können Sie einen Zeiger erstellen, der wie folgt auf die doMultiplication-Funktion zeigt:

pt2Func *myFnPtr = &doMultiplication;

und Sie können die Funktion mit diesem Zeiger wie folgt aufrufen:

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

Das ist eine gute Lektüre: http://www.newty.de/fpt/index.html

73
psychotik

Eine sehr einfache Möglichkeit, typedef des Funktionszeigers zu verstehen:

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

cdecl ist ein großartiges Werkzeug zum Entschlüsseln seltsamer Syntax wie Funktionszeigerdeklarationen. Sie können es auch verwenden, um sie zu generieren.

Was Tipps anbelangt, wie Sie die Analyse komplizierter Deklarationen für zukünftige Wartungsarbeiten (durch Sie selbst oder andere) vereinfachen können, empfehle ich, typedefs aus kleinen Stücken zu erstellen und diese kleinen Stücke als Bausteine ​​für größere und kompliziertere Ausdrücke zu verwenden. Zum Beispiel:

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

eher, als:

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

cdecl kann Ihnen bei diesen Dingen helfen:

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 )

Und genau so habe ich oben dieses verrückte Durcheinander erzeugt.

27
Carl Norum
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;
}

Ausgabe davon ist:

22

6

Beachten Sie, dass für die Deklaration beider Funktionen derselbe math_func-Definer verwendet wurde.

Der gleiche Ansatz von typedef kann für externe Strukturen verwendet werden (mit sturuct in einer anderen Datei).

12

Dies ist das einfachste Beispiel für Funktionszeiger und Funktionszeiger-Arrays, die ich als Übung geschrieben habe.

    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));
    }
4
Bing Bang

Verwenden Sie typedefs, um kompliziertere Typen zu definieren, z. B. Funktionszeiger

Ich nehme das Beispiel der Definition einer Zustandsmaschine in C

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

jetzt haben wir einen Typ namens action_handler definiert, der zwei Zeiger nimmt und ein int zurückgibt

definieren Sie Ihre Zustandsmaschine

    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;

Der Funktionszeiger auf die Aktion sieht aus wie ein einfacher Typ, und typedef erfüllt in erster Linie diesen Zweck.

Alle meine Event-Handler sollten jetzt den von action_handler definierten Typ einhalten

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

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

Verweise:

Kompetente C-Programmierung von Linden

3
vaaz