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!
c
function-pointers
typedef
nbro
źródło
źródło
Odpowiedzi:
Rozważ
signal()
funkcję ze standardu C: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:
możesz zamiast tego zadeklarować
signal()
jako:Oznacza to to samo, ale zwykle uważa się je za nieco łatwiejsze do odczytania. Jest bardziej zrozumiałe, że funkcja przyjmuje an
int
iSignalHandler
a zwraca aSignalHandler
.Trzeba jednak trochę przyzwyczaić się. Jednej rzeczy, której nie można zrobić, jest napisanie funkcji obsługi sygnału za pomocą
SignalHandler
typedef
definicji funkcji.Nadal jestem w starej szkole, która woli wywoływać wskaźnik funkcji, jak:
Nowoczesna składnia używa tylko:
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ł:
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
typedef
wprowadza alias dla typu. Tak więc alias toSignalHandler
, a jego typ to: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:Po utworzeniu typu modułu obsługi sygnału mogę go używać do deklarowania zmiennych i tak dalej. Na przykład:
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. Walarm_handler()
funkcji wartość będzie zawsze,SIGALRM
ponieważ jest to jedyny sygnał, dla którego jest to moduł obsługi, alesignal_handler()
może otrzymaćSIGINT
lubSIGQUIT
jako 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
,SIGPIPE
aSIGTERM
także o tym, czy i one są zdefiniowane (#ifdef
kompilacja warunkowa), ale to tylko komplikuje sprawę. Chciałbym również prawdopodobnie używać POSIXsigaction()
, zamiastsignal()
, ale to już inna kwestia; trzymajmy się tego, od czego zaczęliśmy.W
main()
iteracje funkcyjne na listę ładowarki do zainstalowania. Dla każdegosignal()
modułu obsługi najpierw wywołuje, aby dowiedzieć się, czy proces obecnie ignoruje sygnał, a jednocześnie robi toSIG_IGN
jako moduł obsługi, co zapewnia, że sygnał pozostaje ignorowany. Jeśli sygnał nie był wcześniej ignorowany, wówczas wywołujesignal()
ponownie, tym razem w celu zainstalowania preferowanej procedury obsługi sygnału. (Drugą wartością jest prawdopodobnieSIG_DFL
domyślna procedura obsługi sygnału). Ponieważ pierwsze wywołanie funkcji „signal ()” ustawia funkcję obsługiSIG_IGN
isignal()
zwraca poprzednią procedurę obsługi błędów, wartośćold
poif
instrukcji 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 widziszalarm_handler(1)
, możesz uznać, żealarm_handler
jest to wskaźnik do funkcji, a zatemalarm_handler(1)
jest wywołaniem funkcji za pomocą wskaźnika funkcji.Jak do tej pory pokazałem, że
SignalHandler
zmienna 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ę:
Jeśli zmieniliśmy nazwę funkcji i typ w ten sposób:
to nie masz problemu z interpretacją tego jako funkcja, która trwa
int
idouble
jako argumenty i zwracadouble
wartość (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 przyjmujeSignalHandler
jako drugi argument i zwraca jeden jako wynik.Mechanika, dzięki której można to również traktować jako:
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:
wtedy, kiedy piszesz
var
, reprezentuje wartość podanątype
. Na przykład:Standardowo
typedef
jest traktowane jako gramatyka jako klasa pamięci, a raczej jakstatic
iextern
są to klasy pamięci.oznacza, że gdy zobaczysz zmienną typu
SignalHandler
(np. moduł obsługi alarmu) wywołaną jako:wynik ma
type void
- nie ma wyniku. I(*alarm_handler)(-1);
jest wywołaniealarm_handler()
z argumentem-1
.Więc jeśli zadeklarujemy:
to znaczy, że:
reprezentuje pustą wartość. I dlatego:
jest równoważne. Teraz
signal()
jest bardziej skomplikowany, ponieważ nie tylko zwraca aSignalHandler
, ale także przyjmuje zarównoSignalHandler
argumenty int, jak i jako: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).
źródło
extern void (*signal(int, void(*)(int)))(int);
oznaczasignal(int, void(*)(int))
funkcja zwraca wskaźnik do funkcjivoid 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 przypadkusignal()
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ą. :)&
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.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ę:
następnie następujący typedef:
może być użyty do wskazania tej
doMulitplication
funkcji. 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ż, żept2Func
moż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:
i możesz wywołać funkcję za pomocą tego wskaźnika w następujący sposób:
To sprawia, że dobra lektura: http://www.newty.de/fpt/index.html
źródło
pt2Func myFnPtr = &doMultiplication;
zamiast,pt2Func *myFnPtr = &doMultiplication;
ponieważmyFnPtr
jest to już wskaźnik.myFunPtr
jest już wskaźnikiem funkcji, więc użyjpt2Func myFnPtr = &doMultiplication;
Bardzo łatwy sposób na zrozumienie typedef wskaźnika funkcji:
źródło
cdecl
jest 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
typedef
mał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:zamiast:
cdecl
może ci w tym pomóc:I jest (w rzeczywistości) dokładnie tak, jak wygenerowałem ten szalony bałagan powyżej.
źródło
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).
źródło
Użyj typedefs, aby zdefiniować bardziej skomplikowane typy, np. Wskaźniki funkcji
Weźmię przykład definiowania automatu stanów w C
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
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
Bibliografia:
Programowanie Expert C przez Linden
źródło
Jest to najprostszy przykład wskaźników funkcji i tablic wskaźników funkcji, które napisałem jako ćwiczenie.
źródło