Zrozumienie typedefs dla wskaźników funkcji w C


237

Zawsze byłem trochę zakłopotany, kiedy czytałem kod innych ludzi, który miał typefefy dla wskaźników do funkcji z argumentami. Pamiętam, że zajęło mi trochę czasu, aby przejść do takiej definicji, próbując zrozumieć algorytm numeryczny napisany w C jakiś czas temu. Czy mógłbyś zatem podzielić się swoimi wskazówkami i przemyśleniami na temat pisania dobrych czcionek typograficznych dla wskaźników funkcji (Do i Do nots), dlaczego są one przydatne i jak rozumieć pracę innych? Dzięki!


1
Czy możesz podać jakieś przykłady?
Artelius

2
Czy nie masz na myśli typedefs dla wskaźników funkcji, zamiast makr dla wskaźników funkcji? Widziałem to pierwsze, ale nie drugie.
dave4420,

Odpowiedzi:


297

Rozważ signal()funkcję ze standardu C:

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

Zupełnie niejasne oczywiste - jest to funkcja, która przyjmuje dwa argumenty, liczbę całkowitą i wskaźnik do funkcji, która przyjmuje liczbę całkowitą jako argument i nic nie zwraca, a ( signal()) zwraca wskaźnik do funkcji, która przyjmuje liczbę całkowitą jako argument i zwraca nic.

Jeśli napiszesz:

typedef void (*SignalHandler)(int signum);

możesz zamiast tego zadeklarować signal()jako:

extern  SignalHandler signal(int signum, SignalHandler handler);

Oznacza to to samo, ale zwykle uważa się je za nieco łatwiejsze do odczytania. Jest bardziej zrozumiałe, że funkcja przyjmuje an inti SignalHandlera zwraca a SignalHandler.

Trzeba jednak trochę przyzwyczaić się. Jednej rzeczy, której nie można zrobić, jest napisanie funkcji obsługi sygnału za pomocą SignalHandler typedefdefinicji funkcji.

Nadal jestem w starej szkole, która woli wywoływać wskaźnik funkcji, jak:

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

Nowoczesna składnia używa tylko:

functionpointer(arg1, arg2, ...);

Rozumiem, dlaczego to działa - po prostu wolę wiedzieć, że muszę szukać miejsca, w którym inicjowana jest zmienna, niż wywoływać funkcję functionpointer.


Sam skomentował:

Widziałem już to wyjaśnienie. A potem, tak jak teraz, myślę, że nie dostałem związku między tymi dwoma stwierdzeniami:

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

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

Lub, chcę zapytać, jaka jest podstawowa koncepcja, której można użyć, aby wymyślić drugą wersję, którą masz? Jaka jest podstawa łącząca „SignalHandler” i pierwszy typedef? Myślę, że to, co należy wyjaśnić tutaj, to, co właściwie pisze tutaj typef.

Spróbujmy ponownie. Pierwszy z nich jest podnoszony prosto ze standardu C - przepisałem go i sprawdziłem, czy mam poprawne nawiasy (dopiero po poprawieniu - trudno zapamiętać ciasteczko).

Przede wszystkim pamiętaj, że typedefwprowadza alias dla typu. Tak więc alias to SignalHandler, a jego typ to:

wskaźnik do funkcji, która przyjmuje liczbę całkowitą jako argument i nic nie zwraca.

Część „nic nie zwraca” jest napisana void; argument, który jest liczbą całkowitą, jest (ufam) oczywisty. Poniższa notacja jest po prostu (lub nie) sposobem, w jaki C literuje wskaźnik do funkcji, przyjmując argumenty zgodnie ze specyfikacją i zwracając dany typ:

type (*function)(argtypes);

Po utworzeniu typu modułu obsługi sygnału mogę go używać do deklarowania zmiennych i tak dalej. Na przykład:

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

Uwaga Jak uniknąć używania printf()w module obsługi sygnałów?

Co więc zrobiliśmy tutaj - pomijając 4 standardowe nagłówki, które byłyby potrzebne, aby kod był poprawnie kompilowany?

Pierwsze dwie funkcje to funkcje, które przyjmują jedną liczbę całkowitą i nic nie zwracają. Jeden z nich tak naprawdę wcale nie powraca, exit(1);ale drugi wraca po wydrukowaniu wiadomości. Pamiętaj, że standard C nie pozwala ci robić wiele w procedurze obsługi sygnałów; POSIX jest nieco bardziej hojny w tym, co jest dozwolone, ale oficjalnie nie sankcjonuje połączeń fprintf(). Wydrukowałem również otrzymany numer sygnału. W alarm_handler()funkcji wartość będzie zawsze, SIGALRMponieważ jest to jedyny sygnał, dla którego jest to moduł obsługi, ale signal_handler()może otrzymać SIGINTlub SIGQUITjako numer sygnału, ponieważ ta sama funkcja jest używana w obu przypadkach.

Następnie tworzę tablicę struktur, w której każdy element identyfikuje numer sygnału i procedurę obsługi dla tego sygnału. Wybrałem martwić się o 3 sygnały; Ja często martwić SIGHUP, SIGPIPEa SIGTERMtakże o tym, czy i one są zdefiniowane ( #ifdefkompilacja warunkowa), ale to tylko komplikuje sprawę. Chciałbym również prawdopodobnie używać POSIX sigaction(), zamiast signal(), ale to już inna kwestia; trzymajmy się tego, od czego zaczęliśmy.

W main()iteracje funkcyjne na listę ładowarki do zainstalowania. Dla każdego signal()modułu obsługi najpierw wywołuje, aby dowiedzieć się, czy proces obecnie ignoruje sygnał, a jednocześnie robi to SIG_IGNjako moduł obsługi, co zapewnia, że ​​sygnał pozostaje ignorowany. Jeśli sygnał nie był wcześniej ignorowany, wówczas wywołuje signal()ponownie, tym razem w celu zainstalowania preferowanej procedury obsługi sygnału. (Drugą wartością jest prawdopodobnie SIG_DFLdomyślna procedura obsługi sygnału). Ponieważ pierwsze wywołanie funkcji „signal ()” ustawia funkcję obsługi SIG_IGNi signal()zwraca poprzednią procedurę obsługi błędów, wartość oldpo ifinstrukcji musi być SIG_IGN- stąd twierdzenie. (Cóż, może byćSIG_ERR jeśli coś poszło dramatycznie nie tak - ale wtedy dowiedziałbym się o tym podczas strzelania z twierdzenia).

Następnie program wykonuje swoje czynności i kończy pracę normalnie.

Zauważ, że nazwę funkcji można traktować jako wskaźnik do funkcji odpowiedniego typu. Gdy nie zastosujesz nawiasów wywołania funkcji - jak na przykład w inicjalizatorach - nazwa funkcji staje się wskaźnikiem funkcji. Z tego powodu uzasadnione jest wywoływanie funkcji za pomocą pointertofunction(arg1, arg2)notacji; kiedy widzisz alarm_handler(1), możesz uznać, że alarm_handlerjest to wskaźnik do funkcji, a zatem alarm_handler(1)jest wywołaniem funkcji za pomocą wskaźnika funkcji.

Jak do tej pory pokazałem, że SignalHandlerzmienna jest stosunkowo prosta w użyciu, pod warunkiem, że masz do niej odpowiedni rodzaj wartości - właśnie to zapewniają dwie funkcje modułu obsługi sygnałów.

Teraz wracamy do pytania - jak te dwie deklaracje signal()odnoszą się do siebie.

Przejrzyjmy drugą deklarację:

 extern SignalHandler signal(int signum, SignalHandler handler);

Jeśli zmieniliśmy nazwę funkcji i typ w ten sposób:

 extern double function(int num1, double num2);

to nie masz problemu z interpretacją tego jako funkcja, która trwa inti doublejako argumenty i zwraca doublewartość (to być może, że nie jesteś lepszy „FESS się, czy to jest problematyczne - ale może należy być ostrożnym o zadawanie pytań, jak ciężko jak ten, jeśli jest to problem).

Teraz zamiast być a double, signal()funkcja przyjmuje SignalHandlerjako drugi argument i zwraca jeden jako wynik.

Mechanika, dzięki której można to również traktować jako:

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

są trudne do wyjaśnienia - więc prawdopodobnie to spieprzę. Tym razem podałem nazwy parametrów - choć nazwy nie są krytyczne.

Ogólnie rzecz biorąc, w C mechanizm deklaracji jest taki, że jeśli napiszesz:

type var;

wtedy, kiedy piszesz var, reprezentuje wartość podaną type. Na przykład:

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

Standardowo typedefjest traktowane jako gramatyka jako klasa pamięci, a raczej jak statici externsą to klasy pamięci.

typedef void (*SignalHandler)(int signum);

oznacza, że ​​gdy zobaczysz zmienną typu SignalHandler(np. moduł obsługi alarmu) wywołaną jako:

(*alarm_handler)(-1);

wynik ma type void- nie ma wyniku. I (*alarm_handler)(-1);jest wywołanie alarm_handler()z argumentem -1.

Więc jeśli zadeklarujemy:

extern SignalHandler alt_signal(void);

to znaczy, że:

(*alt_signal)();

reprezentuje pustą wartość. I dlatego:

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

jest równoważne. Teraz signal()jest bardziej skomplikowany, ponieważ nie tylko zwraca a SignalHandler, ale także przyjmuje zarówno SignalHandlerargumenty int, jak i jako:

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

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

Jeśli nadal cię to dezorientuje, nie jestem pewien, jak pomóc - nadal jest dla mnie tajemnicze, ale przyzwyczaiłem się do tego, jak to działa i dlatego mogę powiedzieć, że jeśli będziesz go trzymać przez kolejne 25 lat a przynajmniej stanie się dla ciebie drugą naturą (a może nawet trochę szybciej, jeśli będziesz sprytny).


3
Widziałem już to wyjaśnienie. A potem, tak jak teraz, myślę, że nie dostałem związku między dwiema instrukcjami: extern void ( signal (int, void ( ) (int))) (int); / * i * / typedef void (* SignalHandler) (int signum); zewnętrzny sygnał SignalHandler (int signum, moduł obsługi SignalHandler); Lub, chcę zapytać, jaka jest podstawowa koncepcja, której można użyć, aby wymyślić drugą wersję, którą masz? Jaka jest podstawa łącząca „SignalHandler” i pierwszy typedef? Myślę, że to, co należy wyjaśnić tutaj, to, co właściwie pisze tutaj typef. Thx

6
Świetna odpowiedź, cieszę się, że wróciłem do tego wątku. Nie sądzę, że wszystko rozumiem, ale pewnego dnia to zrozumiem. Właśnie dlatego lubię SO. Dziękuję Ci.
toto

2
Wystarczy wybrać nit: wywołanie printf () i znajomych w module obsługi sygnału nie jest bezpieczne; printf () nie jest wklęsłego (głównie dlatego, że może wywołać malloc (), która nie jest reentrant)
wildplasser

4
W extern void (*signal(int, void(*)(int)))(int);oznacza signal(int, void(*)(int))funkcja zwraca wskaźnik do funkcji void f(int). Gdy chcesz określić wskaźnik funkcji jako wartość zwracaną , składnia staje się skomplikowana. Musisz umieścić typ wartości zwracanej po lewej, a listę argumentów po prawej , podczas gdy to środek , który definiujesz. W tym przypadku signal()sama funkcja przyjmuje wskaźnik funkcji jako parametr, co jeszcze bardziej komplikuje sytuację. Dobra wiadomość jest taka, że ​​jeśli potrafisz to przeczytać, Moc jest już z tobą. :)
smwikipedia,

1
Czego stara szkoła używa &przed nazwą funkcji? Jest to całkowicie niepotrzebne; nawet bez sensu. I zdecydowanie nie „stara szkoła”. Old School używa nazwy funkcji zwykłej i prostej.
Jonathan Leffler,

80

Wskaźnik funkcji jest jak każdy inny wskaźnik, ale wskazuje adres funkcji zamiast adresu danych (na stercie lub stosie). Jak każdy wskaźnik, musi być wpisany poprawnie. Funkcje są określone przez ich wartość zwracaną i typy parametrów, które akceptują. Aby więc w pełni opisać funkcję, musisz dołączyć jej wartość zwracaną, a typ każdego parametru jest akceptowany. Kiedy wpisujesz taką definicję, nadajesz jej „przyjazną nazwę”, która ułatwia tworzenie i odwoływanie się do wskaźników za pomocą tej definicji.

Załóżmy na przykład, że masz funkcję:

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

następnie następujący typedef:

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

może być użyty do wskazania tej doMulitplicationfunkcji. Po prostu definiuje wskaźnik do funkcji, która zwraca liczbę zmiennoprzecinkową i przyjmuje dwa parametry, każdy typu zmiennoprzecinkowego. Ta definicja ma przyjazną nazwę pt2Func. Zauważ, że pt2Funcmoże wskazywać na DOWOLNĄ funkcję, która zwraca liczbę zmiennoprzecinkową i przyjmuje 2 zmiennoprzecinkowe.

Możesz więc utworzyć wskaźnik wskazujący na funkcję doMultiplication w następujący sposób:

pt2Func *myFnPtr = &doMultiplication;

i możesz wywołać funkcję za pomocą tego wskaźnika w następujący sposób:

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

To sprawia, że ​​dobra lektura: http://www.newty.de/fpt/index.html


psychotik, dzięki! To było pomocne. Link do strony wskaźników funkcji jest naprawdę pomocny. Czytam to teraz.

... Jednak ten link newty.de w ogóle nie mówi o typedefs :( Więc chociaż ten link jest świetny, ale odpowiedzi w tym wątku o typedefs są bezcenne!

11
Możesz chcieć to zrobić pt2Func myFnPtr = &doMultiplication;zamiast, pt2Func *myFnPtr = &doMultiplication;ponieważ myFnPtrjest to już wskaźnik.
Tamilselvan,

1
deklarowanie pt2Func * myFnPtr = & doMultiplication; zamiast pt2Func myFnPtr = & doMultiplication; rzuca ostrzeżenie.
AlphaGoku,

2
@Tamilselvan jest poprawny. myFunPtrjest już wskaźnikiem funkcji, więc użyjpt2Func myFnPtr = &doMultiplication;
Dustin Biser

35

Bardzo łatwy sposób na zrozumienie typedef wskaźnika funkcji:

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

cdecljest doskonałym narzędziem do odszyfrowywania dziwnej składni, takiej jak deklaracje wskaźnika funkcji. Możesz go również użyć do ich wygenerowania.

Jeśli chodzi o wskazówki ułatwiające analizowanie skomplikowanych deklaracji w celu przyszłej konserwacji (samodzielnie lub przez innych), zalecam tworzenie typedefmałych fragmentów i wykorzystywanie tych małych elementów jako elementów składowych większych i bardziej skomplikowanych wyrażeń. Na przykład:

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

zamiast:

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

cdecl może ci w tym pomóc:

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 )

I jest (w rzeczywistości) dokładnie tak, jak wygenerowałem ten szalony bałagan powyżej.


2
Cześć Carl, to był bardzo wnikliwy przykład i wyjaśnienie. Dziękujemy również za pokazanie korzystania z cdecl. Bardzo mile widziane.

Czy jest cdecl dla Windows?
Jack

@Jack, jestem pewien, że możesz to zbudować, tak.
Carl Norum

2
Istnieje również cdecl.org, który zapewnia ten sam rodzaj możliwości, ale online. Przydatny dla nas programistów 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;
}

Wynikiem tego jest:

22

6

Zauważ, że do deklarowania obu funkcji użyto tego samego definiatora math_func.

To samo podejście z typedef może być zastosowane do struktury zewnętrznej (przy użyciu robruct w innym pliku).


5

Użyj typedefs, aby zdefiniować bardziej skomplikowane typy, np. Wskaźniki funkcji

Weźmię przykład definiowania automatu stanów w C

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

teraz zdefiniowaliśmy typ o nazwie moduł obsługi akcji, który pobiera dwa wskaźniki i zwraca liczbę całkowitą

zdefiniuj swój automat stanowy

    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;

Wskaźnik funkcji do akcji wygląda jak prosty typ, a typedef służy przede wszystkim do tego celu.

Wszystkie moje moduły obsługi zdarzeń powinny teraz stosować się do typu zdefiniowanego przez moduł obsługi akcji

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

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

Bibliografia:

Programowanie Expert C przez Linden


4

Jest to najprostszy przykład wskaźników funkcji i tablic wskaźników funkcji, które napisałem jako ćwiczenie.

    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));
    }
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.