Jak działają wskaźniki funkcji w C?


1232

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.


34
Ponadto: trochę szczegółowej analizy wskaźników C można znaleźć na blogs.oracle.com/ksplice/entry/the_ksplice_pointer_challenge . Ponadto programowanie od podstaw pokazuje, jak działają na poziomie maszyny. Zrozumienie „modelu pamięci C” jest bardzo przydatne do zrozumienia działania wskaźników C.
Abbafei

8
Świetna informacja. Jednak z tytułu spodziewałbym się, że naprawdę zobaczę wyjaśnienie, w jaki sposób działają „wskaźniki funkcji”, a nie sposób ich kodowania :)
Bogdan Alexandru

Odpowiedzi:


1477

Wskaźniki funkcji w C.

Zacznijmy od podstawowej funkcji, na którą będziemy wskazywać :

int addInt(int n, int m) {
    return n+m;
}

Po pierwsze, zdefiniujmy wskaźnik do funkcji, która otrzymuje 2 ints i zwraca int:

int (*functionPtr)(int,int);

Teraz możemy bezpiecznie wskazać naszą funkcję:

functionPtr = &addInt;

Teraz, gdy mamy wskaźnik do funkcji, użyjmy go:

int sum = (*functionPtr)(2, 3); // sum == 5

Przekazywanie wskaźnika do innej funkcji jest w zasadzie takie samo:

int add2to3(int (*functionPtr)(int, int)) {
    return (*functionPtr)(2, 3);
}

Możemy również używać wskaźników funkcji w wartościach zwracanych (spróbuj nadążyć, robi się bałagan):

// this is a function called functionFactory which receives parameter n
// and returns a pointer to another function which receives two ints
// and it returns another int
int (*functionFactory(int n))(int, int) {
    printf("Got parameter %d", n);
    int (*functionPtr)(int,int) = &addInt;
    return functionPtr;
}

Ale o wiele przyjemniej jest użyć typedef:

typedef int (*myFuncDef)(int, int);
// note that the typedef name is indeed myFuncDef

myFuncDef functionFactory(int n) {
    printf("Got parameter %d", n);
    myFuncDef functionPtr = &addInt;
    return functionPtr;
}

19
Dzięki za wspaniałe informacje. Czy mógłbyś dodać trochę wglądu w to, gdzie używane są wskaźniki funkcji lub które są szczególnie przydatne?
Rich.Carpenter

326
„functionPtr = & addInt;” można również zapisać (i często jest) jako „functionPtr = addInt;” co jest również ważne, ponieważ standard mówi, że nazwa funkcji w tym kontekście jest konwertowana na adres funkcji.
hlovdal

22
hlovdal, w tym kontekście interesujące jest wyjaśnienie, że to właśnie umożliwia napisanie functionPtr = ****************** addInt;
Johannes Schaub - litb

105
@ Rich.Carpenter Wiem, że jest o 4 lata za późno, ale sądzę, że inni ludzie mogą z tego skorzystać: wskaźniki funkcji są przydatne do przekazywania funkcji jako parametrów do innych funkcji . Znalezienie tej odpowiedzi z jakiegoś dziwnego powodu wymagało wielu poszukiwań. Zasadniczo daje C pseudo najwyższej klasy funkcjonalność.
giant91

22
@ Rich.Carpenter: wskaźniki funkcji są przydatne do wykrywania procesora w czasie wykonywania. Posiadaj wiele wersji niektórych funkcji, aby skorzystać z SSE, popcnt, AVX itp. Podczas uruchamiania ustaw wskaźniki funkcji na najlepszą wersję każdej funkcji dla bieżącego procesora. W innym kodzie wystarczy wywołać wskaźnik funkcji, zamiast wszędzie mieć warunkowe rozgałęzienia funkcji procesora. Następnie możesz zastosować skomplikowaną logikę przy podejmowaniu takich decyzji, nawet jeśli procesor ten obsługuje pshufb, jest wolny, więc wcześniejsza implementacja jest jeszcze szybsza. x264 / x265 używają tego szeroko i są open source.
Peter Cordes

303

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:

String s1 = newString();
s1->set(s1, "hello");

Tak, ->i brak newoperatora jest martwym rozdawaniem, ale z pewnością sugeruje, że ustawiamy tekst jakiejś Stringklasy "hello".

Za pomocą wskaźników funkcji, to jest możliwe, aby naśladować metody w C .

Jak to się osiąga?

Ta Stringklasa zawiera wiele structwskaźników funkcji, które działają jak metoda symulowania metod. Poniżej znajduje się częściowa deklaracja Stringklasy:

typedef struct String_Struct* String;

struct String_Struct
{
    char* (*get)(const void* self);
    void (*set)(const void* self, char* value);
    int (*length)(const void* self);
};

char* getString(const void* self);
void setString(const void* self, char* value);
int lengthString(const void* self);

String newString();

Jak można zauważyć, metody Stringklasy są w rzeczywistości wskaźnikami funkcji do deklarowanej funkcji. Przygotowując wystąpienie StringThe newStringfunkcja jest wywoływana w celu ustanowienia tych wskaźników funkcji do ich funkcji:

String newString()
{
    String self = (String)malloc(sizeof(struct String_Struct));

    self->get = &getString;
    self->set = &setString;
    self->length = &lengthString;

    self->set(self, "");

    return self;
}

Na przykład getStringfunkcja wywoływana przez wywołanie getmetody jest zdefiniowana następująco:

char* getString(const void* self_obj)
{
    return ((String)self_obj)->internal->value;
}

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. ( internalJest to tylko ukryty element, structktó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 an ImmutableString. Aby łańcuch stał się niezmienny, setmetoda nie będzie dostępna, przy jednoczesnym zachowaniu dostępu do geti length, i zmusi „konstruktora” do zaakceptowania char*:

typedef struct ImmutableString_Struct* ImmutableString;

struct ImmutableString_Struct
{
    String base;

    char* (*get)(const void* self);
    int (*length)(const void* self);
};

ImmutableString newImmutableString(const char* value);

Zasadniczo dla wszystkich podklas dostępne metody są ponownie wskaźnikami funkcji. Tym razem deklaracja dla setmetody nie jest obecna, dlatego nie można jej wywołać w ImmutableString.

Jeśli chodzi o implementację ImmutableString, jedynym istotnym kodem jest funkcja „konstruktora” newImmutableString:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = self->base->length;

    self->base->set(self->base, (char*)value);

    return self;
}

Podczas tworzenia instancji ImmutableString, wskaźniki funkcji do metod geti lengthfaktycznie odnoszą się do metody String.geti String.length, przechodząc przez basezmienną, która jest wewnętrznie przechowywanym Stringobiektem.

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 lengthmetody, aby 0cały czas ImmutableStringwracała do klasy, wszystko, co należałoby zrobić, to:

  1. Dodaj funkcję, która będzie służyć jako lengthmetoda zastępująca .
  2. Przejdź do „konstruktora” i ustaw wskaźnik funkcji na lengthmetodę przesłonięcia .

Dodanie lengthmetody zastępującej ImmutableStringmożna wykonać, dodając lengthOverrideMethod:

int lengthOverrideMethod(const void* self)
{
    return 0;
}

Następnie wskaźnik funkcji dla lengthmetody w konstruktorze jest podłączony do lengthOverrideMethod:

ImmutableString newImmutableString(const char* value)
{
    ImmutableString self = (ImmutableString)malloc(sizeof(struct ImmutableString_Struct));

    self->base = newString();

    self->get = self->base->get;
    self->length = &lengthOverrideMethod;

    self->base->set(self->base, (char*)value);

    return self;
}

Teraz, zamiast mieć identyczne zachowanie dla lengthmetody w ImmutableStringklasie co Stringklasa, teraz lengthmetoda będzie odwoływać się do zachowania zdefiniowanego w lengthOverrideMethodfunkcji.

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:


22
Ta odpowiedź jest okropna! Nie tylko implikuje, że OO w jakiś sposób zależy od notacji kropkowej, ale także zachęca do wprowadzania śmieci do twoich obiektów!
Alexei Averchenko

27
To jest OO w porządku, ale nie w pobliżu OO w stylu C. To, co zepsułeś, to OO oparte na prototypie JavaScript. Aby uzyskać OO w stylu C ++ / Pascal, musisz: 1. mieć strukturę dla tabeli wirtualnej każdej klasy z elementami wirtualnymi. 2. Mają wskaźnik do tej struktury w obiektach polimorficznych. 3. Wywoływaj metody wirtualne za pośrednictwem wirtualnej tabeli i wszystkich innych metod bezpośrednio - zwykle przestrzegając niektórych ClassName_methodNamekonwencji nazewnictwa funkcji. Tylko wtedy otrzymujesz takie same koszty środowiska wykonawczego i przestrzeni dyskowej, jak w C ++ i Pascal.
Przywróć Monikę

19
Praca z OO w języku, który nie jest OO, jest zawsze złym pomysłem. Jeśli chcesz OO i nadal masz C, po prostu współpracuj z C ++.
rbaleksandar

20
@rbaleksandar Powiedz to deweloperom jądra Linux. „zawsze zły pomysł” jest ściśle twoją opinią, z którą zdecydowanie się nie zgadzam.
Jonathon Reinhart

6
Podoba mi się ta odpowiedź, ale nie rzucam malloc
kot

227

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. 0xC3jest instrukcją x86ret .

Zwykle nie piszesz ich ręcznie, piszesz w języku asemblera, a następnie używasz asemblera, takiego jak nasmassembler, do płaskiego pliku binarnego, który zapisujesz szesnastkowo w literale C.

  1. Zwraca bieżącą wartość z rejestru EAX

    int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
  2. Napisz funkcję wymiany

    int a = 10, b = 20;
    ((void(*)(int*,int*))"\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b")(&a,&b);
  3. Napisz licznik pętli for do 1000, za każdym razem wywołując jakąś funkcję

    ((int(*)())"\x66\x31\xc0\x8b\x5c\x24\x04\x66\x40\x50\xff\xd3\x58\x66\x3d\xe8\x03\x75\xf4\xc3")(&function); // calls function with 1->1000
  4. Możesz nawet napisać funkcję rekurencyjną, która liczy się do 100

    const char* lol = "\x8b\x5c\x24\x4\x3d\xe8\x3\x0\x0\x7e\x2\x31\xc0\x83\xf8\x64\x7d\x6\x40\x53\xff\xd3\x5b\xc3\xc3 <- Recursively calls the function at address lol.";
    i = ((int(*)())(lol))(lol);

Zauważ, że kompilatory umieszczają literały łańcuchowe w .rodatasekcji (lub .rdataw 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()lub VirtualProtect()wywołań systemowych, tak jak potrzebujesz dynamicznie alokowanej pamięci. (Lub gcc -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.

// at global scope
const char swap[] = "\x8b\x44\x24\x04\x8b\x5c\x24\x08\x8b\x00\x8b\x1b\x31\xc3\x31\xd8\x31\xc3\x8b\x4c\x24\x04\x89\x01\x8b\x4c\x24\x08\x89\x19\xc3 <- This swaps the values of a and b";

Kompilując gcc -c -m32 foo.ci dezasemblując z objdump -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.

00000000 <swap>:
   0:   8b 44 24 04             mov    eax,DWORD PTR [esp+0x4]   # load int *a arg from the stack
   4:   8b 5c 24 08             mov    ebx,DWORD PTR [esp+0x8]   # ebx = b
   8:   8b 00                   mov    eax,DWORD PTR [eax]       # dereference: eax = *a
   a:   8b 1b                   mov    ebx,DWORD PTR [ebx]
   c:   31 c3                   xor    ebx,eax                # pointless xor-swap
   e:   31 d8                   xor    eax,ebx                # instead of just storing with opposite registers
  10:   31 c3                   xor    ebx,eax
  12:   8b 4c 24 04             mov    ecx,DWORD PTR [esp+0x4]  # reload a from the stack
  16:   89 01                   mov    DWORD PTR [ecx],eax     # store to *a
  18:   8b 4c 24 08             mov    ecx,DWORD PTR [esp+0x8]
  1c:   89 19                   mov    DWORD PTR [ecx],ebx
  1e:   c3                      ret    

  not shown: the later bytes are ASCII text documentation
  they're not executed by the CPU because the ret instruction sends execution back to the caller

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.


8
Uwaga: nie działa to, jeśli włączona jest funkcja zapobiegania wykonywaniu danych (np. W systemie Windows XP SP2 +), ponieważ ciągi C zwykle nie są oznaczone jako pliki wykonywalne.
SecurityMatt,

5
Cześć Matt! W zależności od poziomu optymalizacji GCC często wstawia stałe ciągów do segmentu TEKST, więc działa to nawet w nowszych wersjach systemu Windows, pod warunkiem, że nie zabrania się tego typu optymalizacji. (IIRC, wersja MINGW w momencie mojego postu ponad dwa lata temu wstawia literały łańcuchowe na domyślnym poziomie optymalizacji)
Lee

10
czy ktoś mógłby wyjaśnić, co się tutaj dzieje? Jakie są te dziwnie wyglądające literały łańcuchowe?
dnia

56
@ajay Wygląda na to, że zapisuje nieprzetworzone wartości szesnastkowe (na przykład „\ x00” jest taki sam jak „/ 0”, oba są równe 0) w łańcuch, następnie rzutuje łańcuch na wskaźnik funkcji C, a następnie wykonuje wskaźnik funkcji C, ponieważ jest diabłem.
ejk314

3
cześć FUZxxl, myślę, że może się różnić w zależności od kompilatora i wersji systemu operacyjnego. Powyższy kod wydaje się działać poprawnie na codepad.org; codepad.org/FMSDQ3ME
Lee

115

Jednym z moich ulubionych zastosowań wskaźników funkcji są tanie i łatwe iteratory -

#include <stdio.h>
#define MAX_COLORS  256

typedef struct {
    char* name;
    int red;
    int green;
    int blue;
} Color;

Color Colors[MAX_COLORS];


void eachColor (void (*fp)(Color *c)) {
    int i;
    for (i=0; i<MAX_COLORS; i++)
        (*fp)(&Colors[i]);
}

void printColor(Color* c) {
    if (c->name)
        printf("%s = %i,%i,%i\n", c->name, c->red, c->green, c->blue);
}

int main() {
    Colors[0].name="red";
    Colors[0].red=255;
    Colors[1].name="blue";
    Colors[1].blue=255;
    Colors[2].name="black";

    eachColor(printColor);
}

7
Powinieneś również przekazać wskaźnik do danych określonych przez użytkownika, jeśli chcesz w jakiś sposób wyodrębnić jakiekolwiek dane wyjściowe z iteracji (pomyśl zamknięcie).
Alexei Averchenko

1
Zgoda. Wszystkie moje iteratorów wyglądać następująco: int (*cb)(void *arg, ...). Zwracana wartość iteratora pozwala mi także zatrzymać się wcześniej (jeśli niezerowa).
Jonathon Reinhart

24

Wskaźniki funkcji stają się łatwe do zadeklarowania, gdy masz już podstawowe deklaratory:

  • ID: ID: ID jest
  • Wskaźnik: *D: D wskaźnik
  • Funkcja: D(<parameters>): D Wykonywanie funkcji <parametry >powrocie

Podczas 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 tak

typedef int ReturnFunction(char);
typedef int ParameterFunction(void);
ReturnFunction *f(ParameterFunction *p);

Jak 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 ]:

function taking 
    [pointer to [function taking [void] returning [int]]] 
returning
    [pointer to [function taking [char] returning [int]]]

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ć:

D1(char);

Wstawiono parametr char bezpośrednio, ponieważ jest to trywialne. Dodanie wskaźnika do deklaratora poprzez zastąpienie D1przez *D2. Pamiętaj, że musimy zawijać nawiasy *D2. Można to poznać, patrząc na pierwszeństwo *-operatoroperatora 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.

(*D2)(char);

Rodzaj zwrotu jest kompletny! Teraz zamieńmy D2na funkcję deklaratora funkcji, <parameters>zwracającą się , do D3(<parameters>)której jesteśmy teraz.

(*D3(<parameters>))(char)

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 z charzastąpionym przez void. Więc skopiuję to:

(*D3(   (*ID1)(void)))(char)

Zastąpiłem D2przez ID1, ponieważ skończyliśmy z tym parametrem (jest to już wskaźnik do funkcji - nie potrzeba innego deklaratora). ID1bę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ę. intOczywiście oba razy .

int (*ID0(int (*ID1)(void)))(char)

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 >powrocie

ID0(<parameters>)

Następną rzeczą w opisie (po „powrocie”) był wskaźnik do . Uwzględnijmy to:

*ID0(<parameters>)

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.

(*ID0(<parameters>))(char)

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ć.

pointer to: *ID1
... function taking void returning: (*ID1)(void)

Po prostu włóż int przed deklarującymi, tak jak zrobiliśmy to z oddolnym podejściem, i jesteśmy skończeni

int (*ID0(int (*ID1)(void)))(char)

Mił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:

int v = (*ID0(some_function_pointer))(some_char);

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ć.


24

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ę printfdo 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

// First, undefine all macros associated with version.h
#undef DEBUG_VERSION
#undef RELEASE_VERSION
#undef INVALID_VERSION


// Define which version we want to use
#define DEBUG_VERSION       // The current version
// #define RELEASE_VERSION  // To be uncommented when finished debugging

#ifndef __VERSION_H_      /* prevent circular inclusions */
    #define __VERSION_H_  /* by using protection macros */
    void board_init();
    void noprintf(const char *c, ...); // mimic the printf prototype
#endif

// Mimics the printf function prototype. This is what I'll actually 
// use to print stuff to the screen
void (* zprintf)(const char*, ...); 

// If debug version, use printf
#ifdef DEBUG_VERSION
    #include <stdio.h>
#endif

// If both debug and release version, error
#ifdef DEBUG_VERSION
#ifdef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

// If neither debug or release version, error
#ifndef DEBUG_VERSION
#ifndef RELEASE_VERSION
    #define INVALID_VERSION
#endif
#endif

#ifdef INVALID_VERSION
    // Won't allow compilation without a valid version define
    #error "Invalid version definition"
#endif

W version.c zdefiniuję 2 prototypy funkcji obecne wversion.h

wersja. c

#include "version.h"

/*****************************************************************************/
/**
* @name board_init
*
* Sets up the application based on the version type defined in version.h.
* Includes allowing or prohibiting printing to STDOUT.
*
* MUST BE CALLED FIRST THING IN MAIN
*
* @return    None
*
*****************************************************************************/
void board_init()
{
    // Assign the print function to the correct function pointer
    #ifdef DEBUG_VERSION
        zprintf = &printf;
    #else
        // Defined below this function
        zprintf = &noprintf;
    #endif
}

/*****************************************************************************/
/**
* @name noprintf
*
* simply returns with no actions performed
*
* @return   None
*
*****************************************************************************/
void noprintf(const char* c, ...)
{
    return;
}

Zauważ, jak wskaźnik funkcji jest prototypowany version.hjako

void (* 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.czwróć uwagę na board_init()funkcję, w której zprintfprzypisano unikalną funkcję (której podpis funkcji pasuje) w zależności od wersji zdefiniowanej wversion.h

zprintf = &printf; zprintf wywołuje printf w celu debugowania

lub

zprintf = &noprint; zprintf po prostu zwraca i nie uruchamia niepotrzebnego kodu

Uruchomienie kodu będzie wyglądać następująco:

mainProg.c

#include "version.h"
#include <stdlib.h>
int main()
{
    // Must run board_init(), which assigns the function
    // pointer to an actual function
    board_init();

    void *ptr = malloc(100); // Allocate 100 bytes of memory
    // malloc returns NULL if unable to allocate the memory.

    if (ptr == NULL)
    {
        zprintf("Unable to allocate memory\n");
        return 1;
    }

    // Other things to do...
    return 0;
}

Powyższy kod będzie używany printfw 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.ha kod zajmie się resztą!


4
U tracisz dużo czasu na wydajność. Zamiast tego możesz użyć makra, które włącza i wyłącza sekcję kodu opartą na debugowaniu / wydaniu.
AlphaGoku,

19

Wskaźnik funkcji jest zwykle definiowany przez typedefi używany jako wartość parametru i wartość zwracana.

Powyższe odpowiedzi wiele już wyjaśniły, podam tylko pełny przykład:

#include <stdio.h>

#define NUM_A 1
#define NUM_B 2

// define a function pointer type
typedef int (*two_num_operation)(int, int);

// an actual standalone function
static int sum(int a, int b) {
    return a + b;
}

// use function pointer as param,
static int sum_via_pointer(int a, int b, two_num_operation funp) {
    return (*funp)(a, b);
}

// use function pointer as return value,
static two_num_operation get_sum_fun() {
    return &sum;
}

// test - use function pointer as variable,
void test_pointer_as_variable() {
    // create a pointer to function,
    two_num_operation sum_p = &sum;
    // call function via pointer
    printf("pointer as variable:\t %d + %d = %d\n", NUM_A, NUM_B, (*sum_p)(NUM_A, NUM_B));
}

// test - use function pointer as param,
void test_pointer_as_param() {
    printf("pointer as param:\t %d + %d = %d\n", NUM_A, NUM_B, sum_via_pointer(NUM_A, NUM_B, &sum));
}

// test - use function pointer as return value,
void test_pointer_as_return_value() {
    printf("pointer as return value:\t %d + %d = %d\n", NUM_A, NUM_B, (*get_sum_fun())(NUM_A, NUM_B));
}

int main() {
    test_pointer_as_variable();
    test_pointer_as_param();
    test_pointer_as_return_value();

    return 0;
}

14

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 qsortibsearch , 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 (albo add()albo sub(), które są tego samego typu), to co zrobimy, dodamy jeden argument wskaźnika funkcji do print()funkcji, jak pokazano poniżej :

#include <stdio.h>

int add()
{
   return (100+10);
}

int sub()
{
   return (100-10);
}

void print(int x, int y, int (*func)())
{
    printf("value is: %d\n", (x+y+(*func)()));
}

int main()
{
    int x=100, y=200;
    print(x,y,add);
    print(x,y,sub);

    return 0;
}

Dane wyjściowe to:

wartość wynosi: 410
wartość wynosi: 390


10

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.

#include<stdio.h>
void (*print)() ;//Declare a  Function Pointers
void sayhello();//Declare The Function Whose Address is to be passed
                //The Functions should Be of Same Type
int main()
{
 print=sayhello;//Addressof sayhello is assigned to print
 print();//print Does A call To The Function 
 return 0;
}

void sayhello()
{
 printf("\n Hello World");
}

wprowadź opis zdjęcia tutajNastę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.


8

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:

int func (int a, char *pStr);    // declares a function

int (*pFunc)(int a, char *pStr);  // declares or defines a function pointer

int (*pFunc2) ();                 // declares or defines a function pointer, no parameter list specified.

int (*pFunc3) (void);             // declares or defines a function pointer, no arguments.

Pierwsze dwie deklaracje są nieco podobne pod tym względem:

  • funcJest to funkcja, która trwa inti A char *i zwracaint
  • pFuncjest wskaźnik funkcji, do których przypisany jest adres funkcji, która pobiera inti A char *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, pFuncjak w pFunc = 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.

int *pfunc(int a, char *pStr);    // declares a function that returns int pointer
int (*pFunc)(int a, char *pStr);  // declares a function pointer that returns an int

Kilka różnych przykładów użycia

Kilka przykładów użycia wskaźnika funkcji:

int (*pFunc) (int a, char *pStr);    // declare a simple function pointer variable
int (*pFunc[55])(int a, char *pStr); // declare an array of 55 function pointers
int (**pFunc)(int a, char *pStr);    // declare a pointer to a function pointer variable
struct {                             // declare a struct that contains a function pointer
    int x22;
    int (*pFunc)(int a, char *pStr);
} thing = {0, func};                 // assign values to the struct variable
char * xF (int x, int (*p)(int a, char *pStr));  // declare a function that has a function pointer as an argument
char * (*pxF) (int x, int (*p)(int a, char *pStr));  // declare a function pointer that points to a function that has a function pointer as an argument

Możesz użyć list parametrów o zmiennej długości w definicji wskaźnika funkcji.

int sum (int a, int b, ...);
int (*psum)(int a, int b, ...);

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.

int  sum ();      // nothing specified in the argument list so could be anything or nothing
int (*psum)();
int  sum2(void);  // void specified in the argument list so no parameters when calling this function
int (*psum2)(void);

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.

int sum (int a, char *b);
int (*psplsum) (int a, int b);
psplsum = sum;               // generates a compiler warning
psplsum = (int (*)(int a, int b)) sum;   // no compiler warning, cast to function pointer
psplsum = (int *(int a, int b)) sum;     // compiler error of bad cast generated, parenthesis are required.

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 ifinstrukcji, chociaż nie jestem pewien, czy byłby użyteczny. Wydaje się, że inne operatory porównania mają jeszcze mniejszą użyteczność.

static int func1(int a, int b) {
    return a + b;
}

static int func2(int a, int b, char *c) {
    return c[0] + a + b;
}

static int func3(int a, int b, char *x) {
    return a + b;
}

static char *func4(int a, int b, char *c, int (*p)())
{
    if (p == func1) {
        p(a, b);
    }
    else if (p == func2) {
        p(a, b, c);      // warning C4047: '==': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
    } else if (p == func3) {
        p(a, b, c);
    }
    return c;
}

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 voidznaczy 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:

int(*p[])() = {       // an array of function pointers
    func1, func2, func3
};
int(**pp)();          // a pointer to a function pointer


p[0](a, b);
p[1](a, b, 0);
p[2](a, b);      // oops, left off the last argument but it compiles anyway.

func4(a, b, 0, func1);
func4(a, b, 0, func2);  // warning C4047: 'function': 'int (__cdecl *)()' differs in levels of indirection from 'char *(__cdecl *)(int,int,char *)'
func4(a, b, 0, func3);

    // iterate over the array elements using an array index
for (i = 0; i < sizeof(p) / sizeof(p[0]); i++) {
    func4(a, b, 0, p[i]);
}
    // iterate over the array elements using a pointer
for (pp = p; pp < p + sizeof(p)/sizeof(p[0]); pp++) {
    (*pp)(a, b, 0);          // pointer to a function pointer so must dereference it.
    func4(a, b, 0, *pp);     // pointer to a function pointer so must dereference it.
}

Styl C namespaceKorzystanie z globalnegostruct ze wskaźnikami funkcji

Możesz użyć staticsłowa kluczowego, aby określić funkcję, której nazwa to zakres pliku, a następnie przypisać ją do zmiennej globalnej, aby zapewnić coś podobnego do namespacefunkcjonalności C ++.

W pliku nagłówkowym zdefiniuj strukturę, która będzie naszą przestrzenią nazw wraz ze zmienną globalną, która z niej korzysta.

typedef struct {
   int (*func1) (int a, int b);             // pointer to function that returns an int
   char *(*func2) (int a, int b, char *c);  // pointer to function that returns a pointer
} FuncThings;

extern const FuncThings FuncThingsGlobal;

Następnie w pliku źródłowym C:

#include "header.h"

// the function names used with these static functions do not need to be the
// same as the struct member names. It's just helpful if they are when trying
// to search for them.
// the static keyword ensures these names are file scope only and not visible
// outside of the file.
static int func1 (int a, int b)
{
    return a + b;
}

static char *func2 (int a, int b, char *c)
{
    c[0] = a % 100; c[1] = b % 50;
    return c;
}

const FuncThings FuncThingsGlobal = {func1, func2};

Będzie to następnie wykorzystane przez podanie pełnej nazwy globalnej zmiennej strukt i nazwy elementu w celu uzyskania dostępu do funkcji. constModyfikator jest stosowany na globalnym tak, że nie mogą być zmienione przez przypadek.

int abcd = FuncThingsGlobal.func1 (a, b);

Obszary zastosowania wskaźników funkcji

Składnik biblioteki DLL może zrobić coś podobnego do namespacepodejścia w stylu C, w którym żądany jest określony interfejs biblioteki z metody fabrycznej w interfejsie biblioteki, który obsługuje tworzenie structwskaź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.

typedef struct {
    HMODULE  hModule;
    int (*Func1)();
    int (*Func2)();
    int(*Func3)(int a, int b);
} LibraryFuncStruct;

int  LoadLibraryFunc LPCTSTR  dllFileName, LibraryFuncStruct *pStruct)
{
    int  retStatus = 0;   // default is an error detected

    pStruct->hModule = LoadLibrary (dllFileName);
    if (pStruct->hModule) {
        pStruct->Func1 = (int (*)()) GetProcAddress (pStruct->hModule, "Func1");
        pStruct->Func2 = (int (*)()) GetProcAddress (pStruct->hModule, "Func2");
        pStruct->Func3 = (int (*)(int a, int b)) GetProcAddress(pStruct->hModule, "Func3");
        retStatus = 1;
    }

    return retStatus;
}

void FreeLibraryFunc (LibraryFuncStruct *pStruct)
{
    if (pStruct->hModule) FreeLibrary (pStruct->hModule);
    pStruct->hModule = 0;
}

i można to wykorzystać jak w:

LibraryFuncStruct myLib = {0};
LoadLibraryFunc (L"library.dll", &myLib);
//  ....
myLib.Func1();
//  ....
FreeLibraryFunc (&myLib);

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 ++.

void * ApplyAlgorithm (void *pArray, size_t sizeItem, size_t nItems, int (*p)(void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for ( ; pList < pListEnd; pList += sizeItem) {
        p (pList);
    }

    return pArray;
}

int pIncrement(int *pI) {
    (*pI)++;

    return 1;
}

void * ApplyFold(void *pArray, size_t sizeItem, size_t nItems, void * pResult, int(*p)(void *, void *)) {
    unsigned char *pList = pArray;
    unsigned char *pListEnd = pList + nItems * sizeItem;
    for (; pList < pListEnd; pList += sizeItem) {
        p(pList, pResult);
    }

    return pArray;
}

int pSummation(int *pI, int *pSum) {
    (*pSum) += *pI;

    return 1;
}

// source code and then lets use our function.
int intList[30] = { 0 }, iSum = 0;

ApplyAlgorithm(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), pIncrement);
ApplyFold(intList, sizeof(int), sizeof(intList) / sizeof(intList[0]), &iSum, pSummation);

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.


0

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 :)

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.