Menu

Wskaźniki na funkcje w C: składnia, callbacki i qsort

Funkcje też mają adresy, a wskaźnik na funkcję przechowuje taki adres. Ta strona rozszyfrowuje składnię deklaracji, pokazuje, jak typedef poprawia czytelność, i dochodzi do callbacków, qsort z własnym komparatorem i tablic dyspozycyjnych.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ