Przychodzę dzisiaj z ciekawym tematem. Będzie to wskaźnik na funkcję. W odróżnieniu od “zwykłych” wskaźników są nieco trudniejsze. Z tego względu, iż funkcje definiuje więcej rzeczy niż zwykłą zmienną.
Wskaźnik na funkcję
Wskaźniki mogą pokazywać na dowolną komórkę pamięci, a nasz kod programu to nic innego jak kolejne instrukcje w pamięci Flash. Można więc na nią wskazać.
Kompilator w kooperacji z linkerem świetnie sobie radzą z wytypowaniem adresu do funkcji.
Funkcja ma pewne określone na stałe elementy. Są nimi argumenty i zwracany wynik. Te rzeczy podajemy z zewnątrz, ale wewnątrz funkcja jest zawsze taka sama. W końcu znajduje się w pamięci stałej – Flash.
Tak naprawdę kod funkcji korzysta z argumentów zawsze w taki sam sposób, w tej samej kolejności i zawsze tak samo zwraca wynik. Po prostu dane “pod spodem” się zmieniają. Operacje są zawsze takie same
Jeśli więc podamy w kodzie adres do funkcji razem z wymaganą przez nią listą argumentów, to tak naprawdę możemy wskazać na dowolną funkcję. Mając dwie funkcje o tych samych argumentach sprawi, iż możemy je wywołać “zamiennie”.
Budowa wskaźnika na funkcję
Jak zbudować wskaźnik na funkcję? Już Ci wspomniałem, iż kompilator musi wiedzieć o całej wymaganej liście argumentów i “zwrotce”. Będzie to musiało być dołączone do wskaźnika.
Kolejnym ważnym elementem jest poinformowanie kompilatora, iż to jest wskaźnik na funkcję. Jak? Dodając operator wywołania funkcji, czyli nawiasy po prawej stronie identyfikatora.
Weźmy przykładową, najprostszą funkcję:
void MyFunction(void) { … }
Mniejsza z tym, co ona robi.
Jak zbudować wskaźnik do niej? Po pierwsze nazwa wskaźnika i zaznaczenie, iż będzie to wskaźnik.
(*FunPtr)
Weźmy od razu w nawiasy, aby nie przeważyły kolejności w łączeniu operatorów. Mamy wskaźnik o nazwie FunPtr. Jeszcze nie wiemy jakiego typu, ani na co.
Ma być to wskaźnik na funkcję, więc musimy dodać operator wywołania funkcji. W taki sam sposób jak pisaliśmy funkcję.
(*FunPtr)()
Jest to już wskaźnik na funkcję. Teraz cała litania argumentów i zwracanego typu. Kompilator MUSI to wiedzieć. Musi wiedzieć, na co DOKŁADNIE pokazuje wskaźnik. Przecież jakoś trzeba przekazać z zewnątrz te argumenty.
Nasza funkcja nic nie przyjmowała i nic nie zwracała, więc jest prosto.
void (*FunPtr)(void)
W argumentach trzeba podać tylko ich typy. W kolejności, jaka jest przyjmowana do funkcji. Teraz możemy przypisać funkcję do wskaźnika.
Sama nazwa funkcji jest rzutowana na adres do niej. To się właśnie wykorzystuje. Nazwa wskaźnika to też adres. Posługujemy się więc samymi nazwami. Poprawność pozostałych elementów sprawdzana jest przez kompilator “w tle”.
FunPtr = MyFunction;
Od teraz wskaźnik FunPtr pokazuje na naszą funkcję. Jak wywołać funkcję spod wskaźnika? Operatorem wywołania funkcji
FunPtr();
Jeśli FunPtr to adres w pamięci, to nawiasy mówią kompilatorowi “wywołaj funkcję spod tego adresu”.
Trudniejszy wskaźnik
Ok to był banał, bo nie mieliśmy argumentów. Zróbmy coś szalonego. Pokażmy na taką przykładową funkcję transferową:
uint8_t ReadFromI2C(uint8_t Address, uint8_t Register, uint8_t *Data, uint16_t Size){…}
Funkcja przyjmuje w argumentach:
- adres urządzenia I2C
- adres rejestru w urządzeniu
- wskaźnik na tablicę do odbioru danych
- ilość odczytywanych danych (np. przy autoinkrementacji)
Zwracany jest kod błędu w postaci uint8_t. jeżeli bez błędu to zero. Jak błąd to coś innego.
Jak zbudować wskaźnik, który pokaże na taką funkcję? Jedziemy!
- Potrzebujemy identyfikator
ReadFromI2CPtr
- Powiedzmy teraz, iż to jest wskaźnik
(*ReadFromI2CPtr)
- Co zwraca wskazywana funkcja
uint8_t (*ReadFromI2CPtr)
- Cała lista argumentów w tej samej kolejności co funkcja na którą chcemy pokazać (same typy)
uint8_t (*ReadFromI2CPtr)(uint8_t, uint8_t, uint8_t*, uint16_t);
I gotowe! Było trudno? Mam nadzieję, iż nie
Przekazanie wskaźnika do funkcji w argumencie
Często taki wskaźnik na funkcję będziemy chcieli przekazać w argumencie. W kolejnym mailu zajmiemy się callbackami i to właśnie przy nich się stosuje wskaźniki na funkcje.
W jaki sposób przekazać taki wskaźnik? Przy wskaźniku na zmienną podawaliśmy typ wskaźnika. Jak tutaj?
Cała ta litania jest typem wskaźnika.
Chcąc przekazać w argumencie wskaźnik na funkcję musimy DOKŁADNIE opisać, jaka to jest funkcja.
Dla przypomnienia funkcję opisują:
- nazwa (adres we Flash)
- typy, kolejność i ilość argumentów
- zwracany typ
Dla przykładu weźmy tę trudniejszą funkcję, a w zasadzie to jej prototyp.
uint8_t ReadFromI2C(uint8_t Address, uint8_t Register, uint8_t *Data, uint16_t Size) { … }
Chcemy adres to tej funkcji przekazać w argumencie. Jak będzie wyglądał prototyp funkcji przyjmującej adres do takiej funkcji?
void MyFun2( ?? ) { … }
Pomyśl chwilę… Musisz przekazać wszystkie informacje na temat wskazywanej funkcji.
Już?
Wiesz?
Skoro wskaźnik na funkcję musi być precyzyjnie określony, to będzie to wyglądało tak samo jakbyśmy tworzyli luźno istniejący wskaźnik.
void MyFun2( uint8_t (*FunctionPtr)(uint8_t, uint8_t, uint8_t*, uint16_t) ){ … }
To jest JEDEN argument! Wskaźnik na funkcję, która zwraca uint8_t i przyjmuje 4 argumenty według podanych typów. Tak, wiem… wygląda to strasznie.
Na szczęście już samo użycie tej funkcji i przekazanie do niej adresu na funkcję jest prostsze. Co jest adresem na funkcję?
Jej nazwa! Więc konkretne wywołanie tej funkcji z koszmarnym argumentem wskaźnikowym będzie wyglądało prościej:
MyFun2( ReadFromI2C );
Tyle. Nazwa funkcji ReadFromI2C jest adresem do niej.
Teraz to kompilator patrzy i analizuje:
“Oookej… funkcja MyFun2 (na podstawie prototypu) przyjmowała adres do funkcji, która zwraca uint8_t i przyjmuje argumenty typu uint8_t, uint8_t, wskaźnik na uint8_t i uint16_t.
Przekazałeś mi gościu adres do jakiejś funkcji ReadFromI2C, więc zerknijmy jak ona wygląda (na podstawie prototypu).
ReadFromI2C (na podstawie prototypu) zwraca uint8_t i przyjmuje argumenty typu uint8_t, uint8_t, wskaźnik na uint8_t i uint16_t.
Wszystko się zgadza! Można kompilować!”
Jeśli wystąpi jakaś niezgodność to kompilator od razu Ci o tym powie!
Kluczowe jest to, aby zrozumieć, iż wskaźnik na funkcję MUSI być bardzo dokładnie opisany. Musi w pełni definiować prototyp funkcji. Jeszcze raz przypomnę, bo to jest ważne:
- nazwa (adres we Flash)
- typy, kolejność i ilość argumentów
- zwracany typ
Najczęstsze błędy
Widuję kilka błędów w próbach posługiwania się wskaźnikami na funkcję. Najważniejsze z nich:
- Brak operatora wywołania funkcji
Jeśli wskaźnik pokazuje na funkcję, to przy jego deklaracji musi być przy nim operator wywołania funkcji.
- Brak nawiasu “odcinającego” wywołanie funkcji od wskaźnika
Nawiasy, czyli operator wywołania funkcji mają WYŻSZY priorytet niż operator wskaźnika, czyli gwiazdka *
Pisząc więc
uint8_t *ReadFromI2CPtr(uint8_t, uint8_t, uint8_t*, uint16_t);
Mamy tak naprawdę prototyp funkcji, która zwraca wskaźnik, a nie deklarację wskaźnika!
Musi być nawias obejmujący gwiazdkę oraz indentyfikator (nazwę) wskaźnika.
uint8_t (*ReadFromI2CPtr)(uint8_t, uint8_t, uint8_t*, uint16_t);
Teraz pierwszeństwo ma wskaźnik, dopiero później operator wywołania funkcji. Teraz jest to wskaźnik na funkcję.
- Niezgodność argumentów
Nie mamy dowolności we wskaźnikach na funkcję. Tak dokładniej mówiąc to nie tworzymy “wskaźnika na funkcję” jako coś ogólnego.
Tworzymy bardzo konkretny “wskaźnik na funkcję, która przyjmuje X1, X2, X3…Xn i zwraca Y. Dlatego niezgodność choćby jednego elementu X lub Y wywali kompilację.
- Używanie nawiasu przy przekazaniu adresu do funkcji
Nawiasy to operator wywołania funkcji. jeżeli chcemy wyciągnąć sam adres do funkcji, to posługujemy się TYLKO NAZWĄ funkcji. Bez nawiasów. Nawiasy spowodują, iż zamiast przekazania adresu… wywołamy funkcję.
Sytuacja jest podobna jak przy wskaźnikach na dane: Sama nazwa = adres. Nazwa z gwiazdką to wartość spod tego adresu.
Dla funkcji sama nazwa = adres. Nazwa z nawiasami to wywołanie funkcji spod tego adresu.
Może się powtarzam, ale to jest najważniejsze w zrozumieniu wskaźników Co jest “tylko” adresem, a co jest “czymś użytecznym”, co chowa się pod tym adresem.,
Podsumowanie
Jak widzisz jest nieco trudniej niż przy zwykłym wskaźniku na dane. Nie ma zmiłuj. W języku C bardzo dużo rzeczy spoczywa na programiście. Wiele rzeczy musi być z góry określone. Między innymi dlatego C jest wydajny
Przestudiuj dokładnie jeszcze raz tego maila. Potestuj w kompilatorze online przypisywanie adresu funkcji do wskaźnika. Wywołaj funkcję przy pomocy wskaźnika. Zrób jakąś skomplikowaną funkcję i próbuj zmieniać tak, aby pokazywały się błędy.
Musisz ćwiczyć i eksperymentować.
Dlaczego tak mocno naciskam na ten wskaźnik na funkcję?
Przyda nam się przy tzw. Callbackach. W powszechnym użyciu wykorzystują one właśnie wskaźniki na funkcję.
Będzie to istotny temat, bo Callbacków w programowaniu mikrokontrolerów używa się dosyć intensywnie.
Daj mi koniecznie znać w komentarzu, czy ten artykuł był dla Ciebie zrozumiały! jeżeli czegoś nie rozumiesz to napisz! To ważne.
Chcesz się nauczyć języka C z myślą o mikrokontrolerach?
Stworzyłem kurs dedykowany mikrokontrolerom. Uczę w nim języka C od podstaw. Wszystko to, co omówiłem w tym wpisie (i wiele, wiele więcej) znajduje się w programie kursu.
Zebrałem swoje doświadczenie z kilku lat programowania embedded i chcę przekazać Ci jak najlepszą wiedzę. Uczestniczyłem w różnych projektach: samodzielnie, start-up, średnia firma i olbrzymia korporacja.
Oprócz podstaw i składni przekazuję masę dobrych praktyk. Wplatam to między tłumaczenie kolejnych aspektów języka C.
Dodatkowym atutem jest również to, iż pokazuję jak można dobrze prowadzić projekt. Pokażę Ci jak radzić sobie z budowaniem warstw abstrakcji. Skorzystamy przy tym ze struktur, wskaźników i callbacków. No i oczywiście podział na pliki. To wiele pomaga.
Takie odseparowane warstwy dużo łatwiej dają się przenosić między projektami, a choćby między różnymi rodzinami mikrokontrolerów.
Dołącz do listy oczekujących na kurs i zacznij naukę razem z przygotowanymi przeze mnie materiałami. Po zapisaniu się będziesz otrzymywał co tydzień maile o języku C: https://cdlamikrokontrolerow.pl