Grundlegendes zu Typedefs für Funktionszeiger in C.

237

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

nbro
quelle
1
Können Sie einige Beispiele nennen?
Artelius
2
Meinen Sie nicht typedefs für Funktionszeiger anstelle von Makros für Funktionszeiger? Ich habe das erstere gesehen, aber nicht das letztere.
Dave4420

Antworten:

296

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

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

Völlig dunkel offensichtlich - es ist eine Funktion, die zwei Argumente akzeptiert, eine Ganzzahl und einen Zeiger auf eine Funktion, die eine Ganzzahl als Argument verwendet und nichts zurückgibt, und sie ( signal()) gibt einen Zeiger auf eine Funktion zurück, die eine Ganzzahl als Argument verwendet und zurückgibt nichts.

Wenn Sie schreiben:

typedef void (*SignalHandler)(int signum);

dann können Sie stattdessen deklarieren signal()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 a intund a nimmt und a SignalHandlerzurückgibt SignalHandler.

Es ist allerdings etwas gewöhnungsbedürftig. Das einzige, was Sie jedoch nicht tun können, ist das Schreiben einer Signalhandlerfunktion unter Verwendung der SignalHandler typedefin der Funktionsdefinition.

Ich gehöre immer noch zur alten Schule, die es vorzieht, einen Funktionszeiger wie folgt aufzurufen:

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

Moderne Syntax verwendet nur:

functionpointer(arg1, arg2, ...);

Ich kann sehen, warum das funktioniert - ich möchte nur lieber wissen, dass ich suchen muss, wo die Variable initialisiert wird, anstatt nach einer aufgerufenen Funktion functionpointer.


Sam kommentierte:

Ich habe diese Erklärung schon einmal gesehen. Und dann, wie jetzt, 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 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, was typedef hier tatsächlich tut.

Lass es uns erneut versuchen. Die erste davon wird direkt aus dem C-Standard entfernt - ich habe sie erneut getippt und überprüft, ob die Klammern richtig waren (erst, nachdem ich sie korrigiert habe - es ist ein schwer zu merkender Cookie).

Denken Sie zunächst daran, dass typedefein Alias ​​für einen Typ eingeführt wird. Der Alias ​​ist also SignalHandlerund sein Typ ist:

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

Der Teil "gibt nichts zurück" wird geschrieben void. Das Argument, das eine ganze Zahl ist, ist (ich vertraue) selbsterklärend. Die folgende Notation ist einfach (oder nicht), wie C einen Zeiger auf eine Funktion buchstabiert, wobei die angegebenen Argumente verwendet und der angegebene Typ zurückgegeben werden:

type (*function)(argtypes);

Nachdem ich den Signalhandlertyp erstellt habe, kann ich damit Variablen deklarieren und so weiter. Beispielsweise:

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 Sie die Verwendung printf()in einem Signalhandler vermeiden können .

Was haben wir hier also getan - abgesehen davon, dass 4 Standard-Header weggelassen wurden, die erforderlich wären, damit der Code sauber kompiliert werden kann?

Die ersten beiden Funktionen sind Funktionen, die eine einzelne Ganzzahl annehmen und nichts zurückgeben. Einer von ihnen kehrt dank dem überhaupt nicht zurück exit(1);, der andere kehrt jedoch nach dem Drucken einer Nachricht zurück. Beachten Sie, dass der C-Standard es Ihnen nicht erlaubt, sehr viel in einem Signalhandler zu tun. POSIX ist etwas großzügiger in Bezug auf das, was erlaubt ist, sanktioniert jedoch offiziell keine Anrufe fprintf(). Ich drucke auch die empfangene Signalnummer aus. In der alarm_handler()Funktion ist der Wert immer SIGALRMso, dass dies das einzige Signal ist, für das es sich um einen Handler handelt, das jedoch signal_handler()möglicherweise SIGINToder erhältSIGQUIT als Signalnummer, da für beide dieselbe 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 mich für 3 Signale entschieden. Ich würde oft Sorgen über SIGHUP, SIGPIPEund SIGTERMauch , und darüber , ob sie (definiert sind #ifdefbedingte Kompilierung), aber das erschwert nur die Dinge. Ich würde wahrscheinlich auch POSIX sigaction()anstelle von verwenden signal(), aber das ist ein anderes Problem. Bleiben wir bei dem, womit wir angefangen haben.

Die main()Funktion durchläuft die Liste der zu installierenden Handler. Für jeden Handler wird zuerst aufgerufen signal(), um herauszufinden, ob der Prozess das Signal derzeit ignoriert, und dabei SIG_IGNals Handler installiert , wodurch sichergestellt wird, dass das Signal ignoriert bleibt. Wenn das Signal zuvor nicht ignoriert wurde, wird es signal()erneut aufgerufen , diesmal um den bevorzugten Signalhandler zu installieren. (Der andere Wert ist vermutlich SIG_DFLder Standard-Signalhandler für das Signal.) Da der erste Aufruf von 'signal ()' den Handler auf den vorherigen Fehlerhandler setzt SIG_IGNund diesen signal()zurückgibt, muss der Wert oldnach der ifAnweisung sein SIG_IGN- daher die Behauptung. (Nun, es könnte seinSIG_ERR wenn etwas dramatisch schief gelaufen ist - aber dann würde ich das aus dem Assert-Firing erfahren.)

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 für Funktionsaufrufe nicht anwenden - wie zum Beispiel bei 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 sehen alarm_handler(1), können Sie davon ausgehen, dass dies alarm_handlerein Zeiger auf die Funktion ist und daher alarm_handler(1)ein Aufruf einer Funktion über einen Funktionszeiger.

Bisher habe ich gezeigt, dass die Verwendung einer SignalHandlerVariablen relativ einfach ist, solange Sie den richtigen Werttyp zuweisen können - genau das bieten die beiden Signalhandlerfunktionen.

Nun kommen wir zurück zu der Frage, in welcher signal()Beziehung die beiden Erklärungen 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 intund ein doubleals Argumente doubleannimmt und einen Wert zurückgibt (oder? Vielleicht sollten Sie sich nicht ein Bild machen, wenn dies problematisch ist - aber vielleicht sollten Sie vorsichtig sein, wenn Sie Fragen so hart stellen wie dieser, wenn es ein Problem ist).

Anstatt a zu sein double, nimmt die signal()Funktion a SignalHandlerals zweites Argument und gibt eins 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 Parameternamen angegeben - obwohl die Namen nicht kritisch sind.

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

type var;

dann, wenn Sie schreiben var, repräsentiert es einen Wert des Gegebenen type. Beispielsweise:

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 typedefwird als Speicherklasse in der Grammatik behandelt, eher wie staticund externsind Speicherklassen.

typedef void (*SignalHandler)(int signum);

bedeutet, dass, wenn Sie eine Variable vom Typ SignalHandler(z. B. alarm_handler) sehen, die wie folgt aufgerufen wird:

(*alarm_handler)(-1);

Das Ergebnis hat type void- es gibt kein Ergebnis. Und (*alarm_handler)(-1);ist eine Anrufung alarm_handler()mit Argument -1.

Also, wenn wir erklärt haben:

extern SignalHandler alt_signal(void);

es bedeutet, dass:

(*alt_signal)();

stellt einen leeren Wert dar. Und deshalb:

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

ist gleichwertig. Jetzt signal()ist es komplexer, weil es nicht nur a zurückgibt SignalHandler, sondern auch sowohl ein int als auch ein a SignalHandlerals 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 ich Ihnen helfen soll - es ist mir immer noch auf einigen Ebenen rätselhaft, aber ich habe mich daran gewöhnt, wie es funktioniert, und kann Ihnen daher sagen, wenn Sie noch 25 Jahre dabei bleiben oder so, es wird für Sie zur zweiten Natur (und vielleicht sogar ein bisschen schneller, wenn Sie klug sind).

Jonathan Leffler
quelle
3
Ich habe diese Erklärung schon einmal gesehen. Und dann, wie es jetzt der Fall ist, denke ich, was ich nicht bekommen habe, war die Verbindung zwischen den beiden Aussagen: extern void ( Signal (int, void ( ) (int)) (int); / * und * / typedef void (* SignalHandler) (int signum); externes SignalHandler-Signal (int signum, SignalHandler-Handler); Oder was ich fragen möchte, 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, was typedef hier tatsächlich tut. Thx
6
Tolle Antwort, ich bin froh, dass ich auf diesen Thread zurückgekommen bin. Ich glaube nicht, dass ich alles verstehe, aber eines Tages werde ich es tun. Deshalb mag ich SO. Danke dir.
toto
2
Nur um eine Nite auszuwählen: Es ist nicht sicher, printf () und Freunde in einem Signalhandler aufzurufen. printf () ist nicht wiedereintrittsfähig (im Grunde genommen, weil es malloc () aufrufen kann, das nicht wiedereintrittsfähig ist)
wildplasser
4
Dies extern void (*signal(int, void(*)(int)))(int);bedeutet, dass die signal(int, void(*)(int))Funktion einen Funktionszeiger zurückgibt void f(int). Wenn Sie einen Funktionszeiger als Rückgabewert angeben möchten , wird die Syntax kompliziert. Sie müssen den Rückgabewerttyp links und die Argumentliste rechts platzieren , während Sie die Mitte definieren. In diesem Fall nimmt die signal()Funktion selbst einen Funktionszeiger als Parameter, was die Sache noch komplizierter macht. Eine gute Nachricht ist, wenn Sie diese lesen können, ist die Macht bereits bei Ihnen. :).
smwikipedia
1
Was ist Old School daran, &vor einem Funktionsnamen zu verwenden? Es ist völlig unnötig; sogar sinnlos. Und definitiv nicht "old school". Die alte Schule verwendet schlicht und einfach einen Funktionsnamen.
Jonathan Leffler
80

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 korrekt eingegeben werden. Funktionen werden durch ihren Rückgabewert und die Arten von Parametern definiert, die sie akzeptieren. Um eine Funktion vollständig zu beschreiben, müssen Sie ihren Rückgabewert angeben und der Typ jedes Parameters wird akzeptiert. Wenn Sie eine solche Definition eingeben, geben Sie ihr einen "Anzeigenamen", der das Erstellen und Verweisen 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 das folgende typedef:

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

kann verwendet werden, um auf diese doMulitplicationFunktion zu verweisen . Es wird einfach ein Zeiger auf eine Funktion definiert, die einen float zurückgibt und zwei Parameter vom Typ float akzeptiert. Diese Definition hat den freundlichen Namen pt2Func. Beachten Sie, dass pt2Funcdies auf JEDE Funktion verweisen kann, die einen Float zurückgibt und 2 Floats aufnimmt.

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

pt2Func *myFnPtr = &doMultiplication;

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

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

Dies macht eine gute Lektüre: http://www.newty.de/fpt/index.html

Psychotik
quelle
Psychotik, danke! Das war hilfreich. Der Link zur Webseite mit Funktionszeigern ist sehr hilfreich. Lies es jetzt.
... Allerdings scheint dieser newty.de-Link überhaupt nicht über typedefs zu sprechen :( Obwohl dieser Link großartig ist, sind die Antworten in diesem Thread über typedefs von unschätzbarem Wert!
11
Möglicherweise möchten Sie dies tun, pt2Func myFnPtr = &doMultiplication;anstatt pt2Func *myFnPtr = &doMultiplication;wie myFnPtrbereits ein Zeiger.
Tamilselvan
1
Deklarieren von pt2Func * myFnPtr = & doMultiplication; anstelle von pt2Func myFnPtr = & doMultiplication; wirft eine Warnung.
AlphaGoku
2
@ Tamilselvan ist richtig. myFunPtrist bereits ein Funktionszeiger, also benutzept2Func myFnPtr = &doMultiplication;
Dustin Biser
35

Ein sehr einfacher Weg, um 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;
}
user2786027
quelle
32

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

In Bezug auf Tipps, wie komplizierte Deklarationen für zukünftige Wartungsarbeiten (von Ihnen selbst oder anderen) einfacher zu analysieren sind, empfehle ich, typedefkleine Stücke zu erstellen und diese kleinen Teile als Bausteine ​​für größere und kompliziertere Ausdrücke zu verwenden. Beispielsweise:

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 dir bei diesem Zeug 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 dieses verrückte Durcheinander oben erzeugt.

Carl Norum
quelle
2
Hallo Carl, das war ein sehr aufschlussreiches Beispiel und eine Erklärung. Vielen Dank auch, dass Sie die Verwendung von cdecl gezeigt haben. Sehr geschätzt.
Gibt es cdecl für Windows?
Jack
@ Jack, ich bin sicher, du kannst es bauen, ja.
Carl Norum
2
Es gibt auch cdecl.org , das die gleichen Funktionen bietet, jedoch online. Nützlich für uns Windows-Entwickler.
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;
}

Ausgabe davon ist:

22

6

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

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

Harshal Doshi Jain
quelle
5

Verwenden Sie typedefs, um kompliziertere Typen zu definieren, dh 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 verwendet 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 dient in erster Linie diesem Zweck.

Alle meine Ereignishandler sollten jetzt dem von action_handler definierten Typ entsprechen

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

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

Verweise:

Expert C Programmierung von Linden

Vaaz
quelle
4

Dies ist das einfachste Beispiel für Funktionszeiger und Funktionszeigerarrays, 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));
    }
Bing Bang
quelle