Skompilowany kod leży w pamięci tak samo jak dane, więc funkcja ma adres. Wskaźnik na funkcję to zmienna, która przechowuje taki adres. A skoro można przechować funkcję, można ją też przekazać jako argument, trzymać ich tablicę albo wybierać między nimi w czasie działania programu.
Na tym opiera się sortowanie według własnej kolejności, obsługa zdarzeń, interfejsy wtyczek i maszyny stanów. To też fragment składni C, przy którym czytelnik najczęściej się zatrzymuje i mruży oczy, więc od niego zaczniemy.
Składnia deklaracji
Weź zwykłą funkcję:
int add(int a, int b);
Żeby zadeklarować wskaźnik, który może przechować jej adres, zastąp nazwę przez (*nazwa):
int (*op)(int, int);
Czytaj od środka: op to wskaźnik, na funkcję, która przyjmuje (int, int) i zwraca int.
Nawiasy nie są ozdobą. Bez nich * wiąże się z typem zwracanym:
int (*f)(int); // wskaźnik na funkcję przyjmującą int i zwracającą int
int *g(int); // FUNKCJA przyjmująca int i zwracająca int*, coś zupełnie innego
Tak to działa od początku do końca:
Obie formy wywołania działają, bo nazwa funkcji automatycznie zamienia się we wskaźnik. Pisz op(10, 3); zapis (*op)(10, 3) to pozostałość po dawnych czasach.
Jedna uwaga do ostatniego printf: %p oczekuje wskaźnika na obiekt, a wskaźniki na funkcje formalnie należą do osobnej rodziny, więc wypisanie takiego wskaźnika nie jest w pełni przenośne. Rzutowanie ucisza kompilatory na platformach, na których to w ogóle działa; rzadko będziesz tego potrzebować.
typedef poprawia czytelność
Składnia deklaracji szybko robi się brzydka, a standardowym lekarstwem jest typedef:
Porównaj int apply(BinaryOp op, int x, int y) z surową formą int apply(int (*op)(int, int), int x, int y). Znaczenie jest to samo, ale tylko jedną z nich przeczytasz jednym rzutem oka. Ogólne zasady znajdziesz na stronie o typedef.
Callbacki: zachowanie dostarcza wywołujący
Callback to funkcja, którą przekazujesz innej funkcji, żeby mogła cię wywołać we właściwym momencie. Dzięki temu jeden fragment kodu zajmuje się strukturą zadania, a wywołujący dostarcza decyzję.
Bez wskaźników na funkcje trzeba by napisać count_even, count_positive i count_big, czyli trzy kopie tej samej pętli. Z nimi pętla powstaje raz, a parametrem jest test.
Prawdziwa korzyść: qsort
Funkcja qsort z biblioteki standardowej to przykład wskaźnika na funkcję, którego naprawdę będziesz używać. Sortuje tablicę czegokolwiek, a udaje się jej to dlatego, że o elementach nie wie nic poza ich rozmiarem i komparatorem, który dostarczasz.
void qsort(void *base, size_t nmemb, size_t size,
int (*compar)(const void *, const void *));
Komparator dostaje dwa const void *, czyli adresy dwóch elementów, i zwraca liczbę ujemną, jeśli pierwszy ma być przed drugim, zero przy remisie i liczbę dodatnią, jeśli ma być po nim.
Z tego komparatora warto wynieść dwa nawyki. Nigdy nie pisz return x - y;: wygląda sprytnie, ale przepełnia się dla dużych albo ujemnych wartości, daje zły znak i źle posortowaną tablicę. Rzutuj przed wyłuskaniem, a nie po nim: *(const int *)a, bo *a na void * nie ma czego odczytać.
Ten sam mechanizm sortuje struktury według dowolnego pola:
Idiom (q->score > p->score) - (q->score < p->score) zwraca dokładnie -1, 0 albo 1 bez odejmowania wartości i bez przepełnienia. A strcmp ma już właściwą konwencję wartości zwracanej, więc komparator łańcuchów mieści się w jednej linii.
Tablice wskaźników na funkcje
Tablica wskaźników na funkcje zamienia łańcuch if/else w wyszukiwanie. Tak interpretery wybierają kod operacji, a systemy menu uruchamiają polecenia.
Dodanie operacji to teraz dodanie wiersza, a nie edycja switch. Połącz to ze strukturami, a dostaniesz standardowy kształt wtyczki w C: struktura wskaźników na funkcje to w tym języku odpowiednik "interfejsu".
Na co uważać
Sygnatury muszą pasować dokładnie. Typ wskaźnika na funkcję obejmuje typy parametrów i typ zwracany. Przypisanie niepasującej funkcji narusza ograniczenia języka, a wywołanie przez wskaźnik złego typu to niezdefiniowane zachowanie, nawet jeśli "działa". Nie rzutuj sygnatury komparatora, żeby uciszyć ostrzeżenie: popraw komparator.
Pusty wskaźnik na funkcję to nadal pusty wskaźnik. Inicjalizuj go wartością NULL i sprawdzaj przed wywołaniem, dokładnie tak jak wskaźniki na dane:
if (handler != NULL) {
handler(event);
}
Na stronie o pustych wskaźnikach przeczytasz, dlaczego to sprawdzenie się opłaca.
Wskaźniki void tracą bezpieczeństwo typów i to ty jesteś systemem typów. Wewnątrz komparatora qsort nic nie powstrzyma cię przed rzutowaniem na zły typ. Jeśli posortujesz tablicę double przez compare_ints, kod skompiluje się bez zastrzeżeń i da bzdury.
Składnia deklaratorów się zagnieżdża. Funkcja zwracająca wskaźnik na funkcję to int (*get_op(char c))(int, int);. Gdy łapiesz się na pisaniu czegoś takiego, sięgnij po typedef:
typedef int (*BinaryOp)(int, int);
BinaryOp get_op(char c); // to samo, ale czytelnie
Najczęściej zadawane pytania
Jak zadeklarować wskaźnik na funkcję w C?
Napisz sygnaturę funkcji z (*nazwa) w miejscu nazwy funkcji: int (*op)(int, int); deklaruje op jako wskaźnik na funkcję, która przyjmuje dwa inty i zwraca int. Nawiasy wokół *op są obowiązkowe: bez nich zadeklarujesz funkcję zwracającą wskaźnik.
Jak wywołać funkcję przez wskaźnik?
Albo op(3, 4), albo (*op)(3, 4). Obie formy są poprawne i robią to samo, bo nazwa funkcji automatycznie zamienia się we wskaźnik. Współczesny kod w C używa prostej formy op(3, 4).
Czym jest callback w C?
Funkcją, którą przekazujesz innej funkcji, żeby mogła cię z powrotem wywołać. Klasyczny przykład to qsort: przekazujesz komparator, a qsort wywołuje go za każdym razem, gdy musi wiedzieć, który z dwóch elementów idzie pierwszy. Dzięki temu jedna procedura sortująca działa dla dowolnego typu.
Czy przy pobieraniu adresu funkcji potrzebne jest &?
Nie. Nazwa funkcji sama zamienia się we wskaźnik na tę funkcję, więc op = add; i op = &add; są równoważne. Większość kodu w C pomija &.