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.
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.
Odpowiedzi:
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 int
s 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;
}
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:
String s1 = newString();
s1->set(s1, "hello");
Tak, ->
i brak new
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 wiele struct
wskaźników funkcji, które działają jak metoda symulowania metod. Poniżej znajduje się częściowa deklaracja String
klasy:
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 String
klasy są w rzeczywistości wskaźnikami funkcji do deklarowanej funkcji. Przygotowując wystąpienie String
The newString
funkcja 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 getString
funkcja wywoływana przez wywołanie get
metody 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. ( 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 an ImmutableString
. Aby łańcuch stał się niezmienny, set
metoda nie będzie dostępna, przy jednoczesnym zachowaniu dostępu do get
i 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 set
metody 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 get
i length
faktycznie odnoszą się do metody String.get
i String.length
, przechodząc przez base
zmienną, która jest wewnętrznie przechowywanym String
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, aby 0
cały czas ImmutableString
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ącej ImmutableString
można wykonać, dodając lengthOverrideMethod
:
int lengthOverrideMethod(const void* self)
{
return 0;
}
Następnie wskaźnik funkcji dla length
metody 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 length
metody w ImmutableString
klasie co String
klasa, teraz length
metoda będzie odwoływać się do zachowania zdefiniowanego w lengthOverrideMethod
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:
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
int eax = ((int(*)())("\xc3 <- This returns the value of the EAX register"))();
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);
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
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 .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()
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.c
i 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.
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);
}
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 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.
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 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.
(*D2)(char);
Rodzaj zwrotu jest kompletny! Teraz zamieńmy D2
na 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 char
zastąpionym przez void
. Więc skopiuję to:
(*D3( (*ID1)(void)))(char)
Zastąpiłem D2
przez ID1
, 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 .
int (*ID0(int (*ID1)(void)))(char)
W tym przykładzie nazwałem identyfikator funkcji ID0
.
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)
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ć.
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:
// 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
#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.h
jako
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.c
zwróć uwagę na board_init()
funkcję, w której zprintf
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 debugowania
lub
zprintf = &noprint;
zprintf po prostu zwraca i nie uruchamia niepotrzebnego kodu
Uruchomienie kodu będzie wyglądać następująco:
#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 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ą!
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:
#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 ∑
}
// test - use function pointer as variable,
void test_pointer_as_variable() {
// create a pointer to function,
two_num_operation sum_p = ∑
// 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;
}
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 (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
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");
}
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.
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:
func
Jest to funkcja, która trwa int
i A char *
i zwracaint
pFunc
jest wskaźnik funkcji, do których przypisany jest adres funkcji, która pobiera int
i 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, pFunc
jak 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 if
instrukcji, 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 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:
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 namespace
Korzystanie z globalnegostruct
ze wskaźnikami funkcji
Moż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 do namespace
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.
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. const
Modyfikator 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 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 tworzenie struct
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.
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.
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 :)