Miałem ostatnio trochę doświadczenia ze wskaźnikami funkcji w C.
Kontynuując tradycję odpowiadania na twoje pytania, postanowiłem zrobić krótkie podsumowanie podstawowych informacji dla tych, którzy potrzebują szybkiej analizy tego tematu.
c
function-pointers
Yuval Adam
źródło
źródło
Odpowiedzi:
Wskaźniki funkcji w C.
Zacznijmy od podstawowej funkcji, na którą będziemy wskazywać :
Po pierwsze, zdefiniujmy wskaźnik do funkcji, która otrzymuje 2
int
s i zwracaint
:Teraz możemy bezpiecznie wskazać naszą funkcję:
Teraz, gdy mamy wskaźnik do funkcji, użyjmy go:
Przekazywanie wskaźnika do innej funkcji jest w zasadzie takie samo:
Możemy również używać wskaźników funkcji w wartościach zwracanych (spróbuj nadążyć, robi się bałagan):
Ale o wiele przyjemniej jest użyć
typedef
:źródło
pshufb
, jest wolny, więc wcześniejsza implementacja jest jeszcze szybsza. x264 / x265 używają tego szeroko i są open source.Wskaźników funkcji w C można używać do programowania obiektowego w C.
Na przykład następujące wiersze są zapisane w C:
Tak,
->
i braknew
operatora jest martwym rozdawaniem, ale z pewnością sugeruje, że ustawiamy tekst jakiejśString
klasy"hello"
.Za pomocą wskaźników funkcji, to jest możliwe, aby naśladować metody w C .
Jak to się osiąga?
Ta
String
klasa zawiera wielestruct
wskaźników funkcji, które działają jak metoda symulowania metod. Poniżej znajduje się częściowa deklaracjaString
klasy:Jak można zauważyć, metody
String
klasy są w rzeczywistości wskaźnikami funkcji do deklarowanej funkcji. Przygotowując wystąpienieString
ThenewString
funkcja jest wywoływana w celu ustanowienia tych wskaźników funkcji do ich funkcji:Na przykład
getString
funkcja wywoływana przez wywołanieget
metody jest zdefiniowana następująco:Jedną z rzeczy, które można zauważyć, jest to, że nie ma pojęcia wystąpienia obiektu i posiadania metod, które faktycznie są częścią obiektu, więc „własny obiekt” musi być przekazywany przy każdym wywołaniu. (
internal
Jest to tylko ukryty element,struct
który został wcześniej pominięty na liście kodów - jest to sposób ukrywania informacji, ale nie dotyczy wskaźników funkcji).Tak więc, zamiast być w stanie to zrobić
s1->set("hello");
, należy przekazać obiekt, aby wykonać akcjęs1->set(s1, "hello")
.Z tym drobne wyjaśnienie konieczności przejścia w odniesieniu do siebie, na uboczu, przejdziemy do następnej części, która jest dziedziczenie w C .
Powiedzmy, że chcemy stworzyć podklasę
String
, powiedzmy anImmutableString
. Aby łańcuch stał się niezmienny,set
metoda nie będzie dostępna, przy jednoczesnym zachowaniu dostępu doget
ilength
, i zmusi „konstruktora” do zaakceptowaniachar*
:Zasadniczo dla wszystkich podklas dostępne metody są ponownie wskaźnikami funkcji. Tym razem deklaracja dla
set
metody nie jest obecna, dlatego nie można jej wywołać wImmutableString
.Jeśli chodzi o implementację
ImmutableString
, jedynym istotnym kodem jest funkcja „konstruktora”newImmutableString
:Podczas tworzenia instancji
ImmutableString
, wskaźniki funkcji do metodget
ilength
faktycznie odnoszą się do metodyString.get
iString.length
, przechodząc przezbase
zmienną, która jest wewnętrznie przechowywanymString
obiektem.Użycie wskaźnika funkcji może osiągnąć dziedziczenie metody z nadklasy.
Możemy dalej kontynuować polimorfizmu C .
Jeśli na przykład z jakiegoś powodu chcielibyśmy zmienić zachowanie
length
metody, aby0
cały czasImmutableString
wracała do klasy, wszystko, co należałoby zrobić, to:length
metoda zastępująca .length
metodę przesłonięcia .Dodanie
length
metody zastępującejImmutableString
można wykonać, dodająclengthOverrideMethod
:Następnie wskaźnik funkcji dla
length
metody w konstruktorze jest podłączony dolengthOverrideMethod
:Teraz, zamiast mieć identyczne zachowanie dla
length
metody wImmutableString
klasie coString
klasa, terazlength
metoda będzie odwoływać się do zachowania zdefiniowanego wlengthOverrideMethod
funkcji.Muszę dodać zastrzeżenie, że wciąż uczę się pisać z obiektowym stylem programowania w C, więc prawdopodobnie są kwestie, których nie wyjaśniłem dobrze, lub które mogą być po prostu złe, jeśli chodzi o to, jak najlepiej wdrożyć OOP w C. Ale moim celem była próba zilustrowania jednego z wielu zastosowań wskaźników funkcji.
Aby uzyskać więcej informacji na temat wykonywania programowania obiektowego w języku C, zapoznaj się z następującymi pytaniami:
źródło
ClassName_methodName
konwencji nazewnictwa funkcji. Tylko wtedy otrzymujesz takie same koszty środowiska wykonawczego i przestrzeni dyskowej, jak w C ++ i Pascal.Przewodnik po zwolnieniu: Jak nadużywać wskaźników funkcji w GCC na maszynach x86, ręcznie kompilując kod:
Te literały łańcuchowe są bajtami 32-bitowego kodu maszynowego x86.
0xC3
jest instrukcją x86ret
.Zwykle nie piszesz ich ręcznie, piszesz w języku asemblera, a następnie używasz asemblera, takiego jak
nasm
assembler, do płaskiego pliku binarnego, który zapisujesz szesnastkowo w literale C.Zwraca bieżącą wartość z rejestru EAX
Napisz funkcję wymiany
Napisz licznik pętli for do 1000, za każdym razem wywołując jakąś funkcję
Możesz nawet napisać funkcję rekurencyjną, która liczy się do 100
Zauważ, że kompilatory umieszczają literały łańcuchowe w
.rodata
sekcji (lub.rdata
w systemie Windows), która jest połączona jako część segmentu tekstowego (wraz z kodem funkcji).Segment tekstowy ma uprawnienia do odczytu i wykonywania, więc rzutowanie literałów łańcuchowych na wskaźniki funkcji działa bez potrzeby
mprotect()
lubVirtualProtect()
wywołań systemowych, tak jak potrzebujesz dynamicznie alokowanej pamięci. (Lubgcc -z execstack
łączy program ze stosem + segment danych + plik wykonywalny sterty, jako szybki hack.)Aby je zdemontować, możesz skompilować to w celu umieszczenia etykiety w bajtach i użyć dezasemblera.
Kompilując
gcc -c -m32 foo.c
i dezasemblując zobjdump -D -rwC -Mintel
, możemy uzyskać asembler i dowiedzieć się, że ten kod narusza ABI, blokując EBX (rejestr zachowany na wywołaniu) i jest ogólnie nieefektywny.Ten kod maszynowy będzie (prawdopodobnie) działał w kodzie 32-bitowym w systemach Windows, Linux, OS X itd.: Domyślne konwencje wywoływania we wszystkich tych systemach operacyjnych przekazują argumenty na stosie zamiast wydajniej w rejestrach. Jednak EBX zachowuje połączenia we wszystkich normalnych konwencjach wywoływania, więc użycie go jako rejestru scratch bez zapisywania / przywracania może łatwo spowodować awarię wywołującego.
źródło
Jednym z moich ulubionych zastosowań wskaźników funkcji są tanie i łatwe iteratory -
źródło
int (*cb)(void *arg, ...)
. Zwracana wartość iteratora pozwala mi także zatrzymać się wcześniej (jeśli niezerowa).Wskaźniki funkcji stają się łatwe do zadeklarowania, gdy masz już podstawowe deklaratory:
ID
: ID jest*D
: D wskaźnikD(<parameters>)
: D Wykonywanie funkcji<
parametry>
powrociePodczas gdy D to kolejny deklarator zbudowany przy użyciu tych samych reguł. W końcu gdzieś kończy się znakiem
ID
(patrz przykład poniżej), który jest nazwą zadeklarowanego bytu. Spróbujmy zbudować funkcję przyjmującą wskaźnik do funkcji, która nie przyjmuje nic i zwraca int, i zwraca wskaźnik do funkcji przyjmującej char i zwracającą int. Z typ-defs jest takJak widać, dość łatwo jest go zbudować za pomocą typedefs. Bez typedefs nie jest to trudne z konsekwentnie stosowanymi powyższymi regułami deklaratora. Jak widzisz, pominąłem część wskazywaną przez wskaźnik i rzecz, którą zwraca funkcja. To pojawia się po lewej stronie deklaracji i nie jest interesujące: jest dodawane na końcu, jeśli już zbudowano deklarator. Zróbmy to. Konsekwentne budowanie, pierwsze podejście - pokazując strukturę za pomocą
[
i]
:Jak widać, można całkowicie opisać typ, dodając deklaratory jeden po drugim. Budowę można wykonać na dwa sposoby. Jeden jest oddolny, zaczynając od bardzo właściwej rzeczy (liści) i przechodząc do identyfikatora. Drugi sposób to z góry na dół, zaczynając od identyfikatora, aż do liści. Pokażę w obie strony.
Od dołu do góry
Budowa zaczyna się od rzeczy po prawej: rzecz zwrócona, czyli funkcja przyjmująca char. Aby rozróżnić deklaratory, zamierzam je ponumerować:
Wstawiono parametr char bezpośrednio, ponieważ jest to trywialne. Dodanie wskaźnika do deklaratora poprzez zastąpienie
D1
przez*D2
. Pamiętaj, że musimy zawijać nawiasy*D2
. Można to poznać, patrząc na pierwszeństwo*-operator
operatora i operatora wywołania funkcji()
. Bez naszych nawiasów kompilator odczytałby to jako*(D2(char p))
. Oczywiście nie byłoby to zwykłe zastąpienie D1*D2
. Nawiasy są zawsze dozwolone wokół deklaratorów. Więc nic złego nie zrobisz, jeśli dodasz ich zbyt dużo.Rodzaj zwrotu jest kompletny! Teraz zamieńmy
D2
na funkcję deklaratora funkcji,<parameters>
zwracającą się , doD3(<parameters>)
której jesteśmy teraz.Zauważ, że nawiasy nie są potrzebne, ponieważ tym razem chcemy
D3
być deklaratorem funkcji, a nie deklaratorem wskaźnika. Świetnie, pozostały tylko parametry. Parametr jest wykonywany dokładnie tak samo, jak zrobiliśmy typ zwracany, tylko zchar
zastąpionym przezvoid
. Więc skopiuję to:Zastąpiłem
D2
przezID1
, ponieważ skończyliśmy z tym parametrem (jest to już wskaźnik do funkcji - nie potrzeba innego deklaratora).ID1
będzie nazwą parametru. Teraz powiedziałem powyżej, że na końcu dodaje się typ, który modyfikują wszystkie te deklaratory - ten, który pojawia się po lewej stronie każdej deklaracji. W przypadku funkcji staje się typem zwracanym. W przypadku wskaźników wskazanych na typ itp. Interesujące jest, gdy zapisany typ, pojawi się w odwrotnej kolejności, po prawej stronie :) W każdym razie zastąpienie go daje pełną deklarację.int
Oczywiście oba razy .W tym przykładzie nazwałem identyfikator funkcji
ID0
.Z góry na dół
Zaczyna się to od identyfikatora po lewej stronie w opisie typu, owijając deklaratora, gdy przechodzimy przez prawą stronę. Zacznij od robienia funkcja
<
parametry>
powrocieNastępną rzeczą w opisie (po „powrocie”) był wskaźnik do . Uwzględnijmy to:
Następnie następna była funkcja zwracania
<
parametrów>
. Ten parametr jest prostym char, więc od razu go wprowadzamy, ponieważ jest naprawdę trywialny.Uwaga nawiasy możemy dodawać, ponieważ znowu chce, że
*
wpierw nie zwiąże, i wtedy(char)
. W przeciwnym razie byłoby to czytać funkcję biorąc<
parametry>
funkcji powracających ... . Nie, funkcje zwracające funkcje nie są nawet dozwolone.Teraz musimy tylko umieścić
<
parametry>
. Pokażę krótką wersję dererwacji, ponieważ myślę, że już teraz masz pomysł, jak to zrobić.Po prostu włóż
int
przed deklarującymi, tak jak zrobiliśmy to z oddolnym podejściem, i jesteśmy skończeniMiła rzecz
Czy lepiej jest oddolnie czy odgórnie? Jestem przyzwyczajony do oddolnego, ale niektórzy ludzie mogą czuć się bardziej komfortowo z odgórnym. Myślę, że to kwestia gustu. Nawiasem mówiąc, jeśli zastosujesz wszystkich operatorów w tej deklaracji, otrzymasz int:
To ładna właściwość deklaracji w C: Deklaracja stwierdza, że jeśli te operatory są używane w wyrażeniu używającym identyfikatora, to daje typ po lewej stronie. Tak samo jest z tablicami.
Mam nadzieję, że podoba Ci się ten mały samouczek! Teraz możemy połączyć się z tym, gdy ludzie zastanawiają się nad dziwną składnią deklaracji funkcji. Próbowałem umieścić jak najmniej elementów wewnętrznych C. Możesz go edytować / naprawiać.
źródło
Kolejne dobre zastosowanie wskaźników funkcji:
przełączanie między wersjami
Są bardzo przydatne, gdy chcesz mieć różne funkcje w różnych momentach lub na różnych etapach rozwoju. Na przykład tworzę aplikację na komputerze hosta, który ma konsolę, ale ostateczna wersja oprogramowania zostanie umieszczona na płycie Avnet ZedBoard (która ma porty dla wyświetlaczy i konsol, ale nie są potrzebne / potrzebne dla wersja ostateczna). Więc podczas programowania użyję
printf
do wyświetlania komunikatów o stanie i komunikatach o błędach, ale kiedy skończę, nie chcę niczego drukować. Oto co zrobiłem:wersja. h
W
version.c
zdefiniuję 2 prototypy funkcji obecne wversion.h
wersja. c
Zauważ, jak wskaźnik funkcji jest prototypowany
version.h
jakovoid (* zprintf)(const char *, ...);
Gdy zostanie przywołane w aplikacji, zacznie działać wszędzie tam, gdzie wskazuje, co jeszcze nie zostało zdefiniowane.
W
version.c
zwróć uwagę naboard_init()
funkcję, w którejzprintf
przypisano unikalną funkcję (której podpis funkcji pasuje) w zależności od wersji zdefiniowanej wversion.h
zprintf = &printf;
zprintf wywołuje printf w celu debugowanialub
zprintf = &noprint;
zprintf po prostu zwraca i nie uruchamia niepotrzebnego koduUruchomienie kodu będzie wyglądać następująco:
mainProg.c
Powyższy kod będzie używany
printf
w trybie debugowania lub nie będzie nic robić w trybie zwolnienia. Jest to o wiele łatwiejsze niż przeglądanie całego projektu i komentowanie lub usuwanie kodu. Wszystko, co muszę zrobić, to zmienić wersję,version.h
a kod zajmie się resztą!źródło
Wskaźnik funkcji jest zwykle definiowany przez
typedef
i używany jako wartość parametru i wartość zwracana.Powyższe odpowiedzi wiele już wyjaśniły, podam tylko pełny przykład:
źródło
Jednym z dużych zastosowań wskaźników funkcji w C jest wywołanie funkcji wybranej w czasie wykonywania. Na przykład biblioteka czasu wykonania C ma dwie procedury
qsort
ibsearch
, które odbywają wskaźnik do funkcji, która nazywa się porównać dwa elementy są sortowane; pozwala to odpowiednio sortować lub wyszukiwać dowolne dane w oparciu o dowolne kryteria.Bardzo prosty przykład, jeśli wywoływana jest jedna funkcja,
print(int x, int y)
która z kolei może wymagać wywołania funkcji (alboadd()
albosub()
, które są tego samego typu), to co zrobimy, dodamy jeden argument wskaźnika funkcji doprint()
funkcji, jak pokazano poniżej :Dane wyjściowe to:
źródło
Funkcja rozpoczynania od zera ma pewien adres pamięci od miejsca, w którym zaczynają działać. W języku asemblera są one wywoływane jako (wywołanie „adres pamięci funkcji”). Teraz wróć do C Jeśli funkcja ma adres pamięci, wówczas można nimi manipulować za pomocą wskaźników w C. Tak więc zgodnie z regułami C
1. Najpierw musisz zadeklarować wskaźnik do funkcji 2. Podaj adres żądanej funkcji
**** Uwaga-> funkcje powinny być tego samego typu ****
Ten prosty program zilustruje każdą rzecz.
Następnie pozwala zobaczyć, jak maszyna rozumie je. Zobacz instrukcje maszynowe powyższego programu w architekturze 32-bitowej.
Obszar czerwonego znaku pokazuje, w jaki sposób adres jest wymieniany i zapisywany w eax. To jest instrukcja połączenia na eax. eax zawiera pożądany adres funkcji.
źródło
Wskaźnik funkcji to zmienna zawierająca adres funkcji. Ponieważ jest to zmienna wskaźnikowa, ale z pewnymi ograniczonymi właściwościami, możesz jej używać prawie tak, jak każdej innej zmiennej wskaźnikowej w strukturach danych.
Jedynym wyjątkiem, jaki mogę wymyślić, jest traktowanie wskaźnika funkcji jako wskazującego na coś innego niż pojedynczą wartość. Wykonywanie arytmetyki wskaźnika poprzez zwiększanie lub zmniejszanie wskaźnika funkcji lub dodawanie / odejmowanie przesunięcia wskaźnika wskaźnika nie jest tak naprawdę użytecznym narzędziem, ponieważ wskaźnik funkcji wskazuje tylko na jedną rzecz, punkt wejścia funkcji.
Rozmiar zmiennej wskaźnika funkcji, liczba bajtów zajmowanych przez zmienną, może się różnić w zależności od podstawowej architektury, np. X32 lub x64 lub cokolwiek innego.
Deklaracja zmiennej wskaźnika funkcji musi określać ten sam rodzaj informacji, co deklaracja funkcji, aby kompilator C mógł przeprowadzać takie sprawdzenia, jak zwykle. Jeśli nie podasz listy parametrów w deklaracji / definicji wskaźnika funkcji, kompilator C nie będzie mógł sprawdzić użycia parametrów. Zdarzają się przypadki, w których ten brak kontroli może być przydatny, ale pamiętaj tylko o usunięciu siatki bezpieczeństwa.
Kilka przykładów:
Pierwsze dwie deklaracje są nieco podobne pod tym względem:
func
Jest to funkcja, która trwaint
i Achar *
i zwracaint
pFunc
jest wskaźnik funkcji, do których przypisany jest adres funkcji, która pobieraint
i Achar *
i zwracaint
Tak więc z powyższego możemy mieć linię źródłową, w której adres funkcji
func()
jest przypisany do zmiennej wskaźnika funkcji,pFunc
jak wpFunc = func;
.Zwróć uwagę na składnię używaną z deklaracją / definicją wskaźnika funkcji, w której nawiasy są używane do przezwyciężenia reguł pierwszeństwa operatorów naturalnych.
Kilka różnych przykładów użycia
Kilka przykładów użycia wskaźnika funkcji:
Możesz użyć list parametrów o zmiennej długości w definicji wskaźnika funkcji.
Lub nie możesz w ogóle określić listy parametrów. Może to być przydatne, ale eliminuje możliwość przeprowadzania przez kompilator C sprawdzania podanej listy argumentów.
Odlewy w stylu C.
Możesz używać rzutów w stylu C ze wskaźnikami funkcji. Należy jednak pamiętać, że kompilator języka C może mieć luźne podejście do sprawdzania lub zapewniać ostrzeżenia zamiast błędów.
Porównaj wskaźnik funkcji do równości
Możesz sprawdzić, czy wskaźnik funkcji jest równy określonemu adresowi funkcji, używając
if
instrukcji, chociaż nie jestem pewien, czy byłby użyteczny. Wydaje się, że inne operatory porównania mają jeszcze mniejszą użyteczność.Tablica wskaźników funkcji
A jeśli chcesz mieć tablicę wskaźników funkcji, z których każdy element różni się na liście argumentów, możesz zdefiniować wskaźnik funkcji z nieokreśloną listą argumentów (nie
void
znaczy to, że nie ma argumentów, ale tylko nieokreślony). może zobaczyć ostrzeżenia z kompilatora C. Działa to również w przypadku parametru wskaźnika funkcji do funkcji:Styl C
namespace
Korzystanie z globalnegostruct
ze wskaźnikami funkcjiMożesz użyć
static
słowa kluczowego, aby określić funkcję, której nazwa to zakres pliku, a następnie przypisać ją do zmiennej globalnej, aby zapewnić coś podobnego donamespace
funkcjonalności C ++.W pliku nagłówkowym zdefiniuj strukturę, która będzie naszą przestrzenią nazw wraz ze zmienną globalną, która z niej korzysta.
Następnie w pliku źródłowym C:
Będzie to następnie wykorzystane przez podanie pełnej nazwy globalnej zmiennej strukt i nazwy elementu w celu uzyskania dostępu do funkcji.
const
Modyfikator jest stosowany na globalnym tak, że nie mogą być zmienione przez przypadek.Obszary zastosowania wskaźników funkcji
Składnik biblioteki DLL może zrobić coś podobnego do
namespace
podejścia w stylu C, w którym żądany jest określony interfejs biblioteki z metody fabrycznej w interfejsie biblioteki, który obsługuje tworzeniestruct
wskaźników funkcji zawierających. Ten interfejs biblioteki ładuje żądaną wersję biblioteki DLL, tworzy struct z niezbędnymi wskaźnikami funkcji, a następnie zwraca struct do żądającego obiektu wywołującego do użycia.i można to wykorzystać jak w:
To samo podejście można zastosować do zdefiniowania abstrakcyjnej warstwy sprzętowej dla kodu, który wykorzystuje określony model podstawowego sprzętu. Wskaźniki funkcji są fabrycznie wypełnione funkcjami specyficznymi dla sprzętu, aby zapewnić funkcjonalność specyficzną dla sprzętu, która implementuje funkcje określone w abstrakcyjnym modelu sprzętu. Może to być wykorzystane do zapewnienia abstrakcyjnej warstwy sprzętowej używanej przez oprogramowanie, które wywołuje funkcję fabryczną w celu uzyskania określonego interfejsu funkcji sprzętowej, a następnie używa dostarczonych wskaźników funkcji do wykonywania działań na sprzęcie bazowym bez konieczności znajomości szczegółów implementacji dotyczących określonego celu .
Wskaźniki funkcji do tworzenia delegatów, handlerów i oddzwaniania
Wskaźników funkcji można użyć jako sposobu delegowania niektórych zadań lub funkcji. Klasycznym przykładem w C jest wskaźnik funkcji delegowania porównania używany ze standardowymi funkcjami biblioteki C
qsort()
ibsearch()
zapewniający porządek sortowania do sortowania listy elementów lub wyszukiwania binarnego nad posortowaną listą elementów. Delegat funkcji porównania określa algorytm sortowania używany w sortowaniu lub wyszukiwaniu binarnym.Inne zastosowanie jest podobne do zastosowania algorytmu do kontenera Standardowa biblioteka szablonów C ++.
Innym przykładem jest kod źródłowy GUI, w którym rejestrowany jest moduł obsługi określonego zdarzenia poprzez dostarczenie wskaźnika funkcji, który jest faktycznie wywoływany, gdy zdarzenie ma miejsce. Struktura Microsoft MFC z mapami komunikatów używa czegoś podobnego do obsługi komunikatów Windows dostarczanych do okna lub wątku.
Funkcje asynchroniczne wymagające wywołania zwrotnego są podobne do procedury obsługi zdarzeń. Użytkownik funkcji asynchronicznej wywołuje funkcję asynchroniczną w celu rozpoczęcia akcji i udostępnia wskaźnik funkcji, który funkcja asynchroniczna wywoła po zakończeniu akcji. W tym przypadku zdarzeniem jest funkcja asynchroniczna realizująca swoje zadanie.
źródło
Ponieważ wskaźniki funkcji są często typowymi wywołaniami zwrotnymi, warto przyjrzeć się bezpiecznym wywołaniom zwrotnym typu . To samo dotyczy punktów wejścia itp. Funkcji, które nie są wywołaniami zwrotnymi.
C jest dość kapryśny i wybaczający zarazem :)
źródło