Funkcje
Z niecierpliwością czekałem na tę lekcję. Funkcje są stosowane niemal wszędzie, dlatego też ich znajomość bardzo się przydaje. Na początku kursu trochę nadmieniłem o funkcji main. Zatem już wiesz, że funkcja musi posiadać nazwę. Pamiętasz też, że pisząc nazwę należy unikać pewnych słów i znaków. Podczas pisania nazwy funkcji obowiązują takie same zasady jak przy zmiennych. Czyli jedna definicja i wiele deklaracji. Deklaracja może w ogóle nie nastąpić, bo jak pamiętasz definicja jest również deklaracją. Tyle przypomnienia na początek. Teraz powiemy sobie dokładnie czym są funkcje i jak je stosować.
Funkcja to nic innego, jak fragment kodu, który można wywoływać w dowolnym momencie. Funkcje mają na celu przede wszystkim skrócenie programów, ale nie tylko. Są one bardzo poręczne. Wyobraź sobie taką sytuację. W programie wykonujesz bardzo często pewną część kodu. Zajmuje on powiedzmy 30 linijek kodu. Korzystasz z niego 100, 1000, 10000 razy! Zatem w pamięci komputera musi istnieć 10000 tych samych instrukcji :-O Cholerne marnotrawstwo. Poza tym umieszczanie za każdym razem tych 30 linii kodu jest dość nie wygodne. Dlatego właśnie powstały funkcje. Taka funkcja składa się z określonych instrukcji. Funkcja znajduje się w pamięci jednokrotnie, ale możesz z niej korzystać nieograniczoną ilość razy. Chyba widzisz, że jest to korzystne. Szczególnie przy milionach wywołań. Normalnymi sposobami najzwyczajniej brakłoby pamięci. Na pewno zetknąłeś się już kiedyś z pojęciem funkcji. Choćby w szkole na lekcjach matmy. Kojarzysz jeszcze funkcję liniową? Pewnie tak, ale na wszelki wypadek przypomnijmy. Ogólny wzór tej funkcji to: y = ax + b. Wartość funkcji czyli u nas y zależy tutaj przede wszystkim od argumentu x. Argument x może być dowolną liczbą lub wyrażeniem. Dla ułatwienia przyjmijmy, że jest on liczbą. W programowaniu funkcje działają analogicznie. Podajemy pewien argument [w naszym przypadku był to x]. Po dokonaniu przeliczeń funkcja zwraca rezultat [u nas y]. Oczywiście nie zawsze funkcja musi coś zwracać. Jeżeli służy ona do np. narysowania elipsy. Wprawdzie może dodatkowo zwracać obwód, czy pole elipsy, ale to jakby tylko dodatek. W takich sytuacjach funkcja nic nie zwróci. Prawdę mówiąc trochę oszukuje mówiąc, że funkcja nie zwróci nic. Bowiem każda funkcja zwraca pewien rezultat :-/ Już wyjaśniam. Pamiętasz jeszcze rodzaje zmiennych? Nadmieniłem tam o typie void. Powiedziałem też, że stosuje się go głównie w funkcjach. Słowo void oznacza pusty. A zatem funkcja rysująca elipsę zwróci jako rezultat właśnie ten typ. Zwróci obiekt, który tak naprawdę nie przedstawia żadnej wartości. Jest jak mówi nazwa pusty. Początkowo może się to wydawać nieco dziwne i zagmatwane, ale przyzwyczaisz się. Zobacz, jak wygląda definicja przykładowej funkcji:
1 2 3 4 5 6 7 |
void funkcja() { //ciało funkcji } |
Hmm. Co my tu mamy? Powyższy przykład jest funkcją. Zwróć uwagę na składnię. Przy definiowaniu funkcji najpierw piszemy rodzaj zwracanej wartości. Następnie jest nazwa funkcji oraz para nawiasów okrągłych. W nich są umieszczane argumenty wywołania funkcji. Argumenty to dowolne zmienne przekazywane do funkcji w momencie jej wywołania. Nasza funkcja jest wywoływana bez żadnych argumentów. Ciało funkcji, czyli instrukcje zawarte pomiędzy klamrami jest puste. Rezultatem jest typ void. Zatem nasza funkcja nie robi nic prócz tego, ze istnieje 🙂 Ma ona oczywiście zerowe zastosowanie. Służy jedynie, jako przykład. Pobawmy się trochę i rozbudujmy ją!
1 2 3 4 5 6 7 8 9 |
int funkcja(int x) { int c = (x + 2); return c; } |
Tym razem funkcja przyjmuje jeden argument o umownej nazwie x. Jest on typy int. W przeciwieństwie do poprzedniczki tutaj wykonujemy pewne obliczenia. Wewnątrz funkcji definiujemy zmienną typu int o nazwie c. Jest ona już na starcie inicjalizowana wartością c powiększoną o liczbę dwa. Dodatkowo widnieje tam także instrukcja return. Samo słowo oznacza powrót. W naszym przypadku przy powrocie następuje jeszcze zwróceni wartości zmiennej c. Teraz popatrz jak można skorzystać z takiej funkcji:
1 2 3 4 5 |
int zmienna; zmienna = 10; zmienna = funkcja(zmienna); |
Na początku zdefiniowaliśmy sobie zmienną. Zawiera ona jakieś przypadkowe dane. Dalej podstawiamy do niej wartość 10. Teraz uważaj. Następuje wywołanie funkcji. Jako argument podajemy wartość zmiennej, czyli 10. Wewnątrz funkcji definiowana jest inna zmienna o nazwie c. Jest ona inicjalizowana argumentem funkcji powiększonym o dwa [u nas 10 + 2]. Na końcu jest zwracana wartość zmiennej c. Od tej chwili wartośc zmiennej wynosi 12. Rozumiesz jak to działa? Pokażę to jeszcze raz na wspomnianej funkcji liniowej.
1 2 3 4 5 6 7 8 9 |
float funkcja_liniowa(float a, float b, float x) { float rezultat = ((a * x) + b); return rezultat; } |
Powyższa funkcja jest bardziej przydatna. Na podstawie kilku danych może wyliczyć wartość funkcji liniowej. Jako argumenty należy podać kolejno wartość czynnika c i a oraz argument x. Te informacje są niezbędne do wyliczenia wartości funkcji. Tym razem wywołanie wygląda tak:
1 |
float y = funkcja_liniowa(3.0, 5.6, 6.0); |
Pamiętaj, że podczas wywoływania funkcji podajesz konkretne nazwy zmiennych lub jak w przykładzie konkretne liczby. Rodzaje wartości ustalasz tylko przy definicji funkcji. W naszym przykładzie zdefiniowaliśmy sobie zmienną typu float o nazwie y. Już na początku została ona zainicjalizowana wartością naszej funkcji_liniowej. Tę funkcje wywołaliśmy z trzema argumentami: 3.0, 5.6 i 6.0. Pamiętaj, że argumenty oddziela się przecinkami. Weźmy teraz inną funkcję.
1 2 3 4 5 6 7 8 9 |
void funkcja(int x, int y, int promien) { if(promien > 100) return; //instrukcje rysujące elipsę } |
Zauważ, że funkcja zwraca typ void, czyli tak na prawdę nic nie zwraca. Słowo return jest więc zbędne. Jednak można je umieścić, ale nie wolno postawić przy nim żadnej wartości. Tylko średnik. Próba postawienia tam czego innego skończy się buntem kompilatora. Taka pusta instrukcja return też ma swoje zastosowanie. W naszym przypadku w momencie, gdy promień będzie większy od stu elipsa nie zostanie narysowana. Tym samym funkcja zakończy się. Wszystko za sprawą instrukcji return. Napiszmy jeszcze jedną funkcję.
1 2 3 4 5 6 7 |
void funkcja(int argument) { argument = argument * 100; } |
Funkcja jak każda inna. Przyjmuje jeden argument i nic nie zwraca. Najciekawsze jest ciało funkji. Spójrz co tam jest wykonywane. Przysłany argument jest mnożony przez sto i to wszystko. Wywołanie funkcji jest nieco inne, gdyż funkcja nic nie zwraca. Wygląda ono tak:
1 2 3 |
int zmienna = 10; //zmienna = funkcja(); funkcja(zmienna); |
Na przykładzie jedną linijkę musiałem ująć w komentarz, gdyż kompilator by protestował. Jest to oczywiste, ponieważ funkcja nie zwraca nic, zatem powyższy zapis jest błędem. Następnie jest wywołanie funkcji z argumentem zmienna. Teraz pomyśl chwilę. Jaka jest wartość zmiennej, skoro na początku wynosiła 10? Jeżeli pomyślałeś, że 1000 to trochę się pomyliłeś 🙁 Wartość zmiennej nie zmieniła się :-/ Przeanalizujmy teraz jak to działa. Najpierw zdefiniowaliśmy sobie zmienną, którą zainicjalizowaliśmy wartością 10. Teraz wywołujemy naszą funkcję z argumentem będącym zmienną [czyli 10]. Tutaj jest najważniejsze. Przy wejściu do funkcji jest tworzona kopia tego, co zostało wysłane do funkcji. My wysłaliśmy wartość zmiennej. Zatem powstał teraz obiekt chwilowy zainicjalizowany argumentem funkcji, czyli wartością 10. W ciele funkcji następuje wymnożenie tego chwilowego obiektu przez sto. Tuż przed zakończeniem funkcji obiekt chwilowy jest usuwany i kończy się jego żywot 🙂 Zatem nasza zmienna została nietknięta. Tutaj nastąpiło przesłanie argumentu przez wartość. Oznacza to, że funkcja pracuje na kopii argumentu. Sami jesteśmy sobie winni. Oczywiście istnieje możliwość poinformowania funkcji aby pracowała na oryginale. Wtedy mówimy o przesyłaniu argumentu przez referencję. Nawet nie trzeba specjalnie modyfikować kodu. Wystarczy jedynie dodać znaczek ampersand [&] w definicji w taki sposób:
1 2 3 4 5 6 7 |
void funkcja(int &argument) { argument = argument * 100; } |
Teraz funkcja nie tworzy już obiektu chwilowego. Pracuje na oryginale. Zatem jeśli wywołamy ją tak jak poprzednio, to wartość zmiennej rzeczywiście wyniesie 1000.
1 2 3 |
int zmienna = 10; funkcja(zmienna); |
Wystarczył jeden znaczek i funkcja działa tak jak chcieliśmy. Wywołanie jest identyczne. Teraz zajmiemy się wnętrzem samej funkcji. Być może spotkałeś się kiedyś z pojęciem stosu. Stos to pewien obszar pamięci, na którym umieszczane są wszelkie chwilowe dane. Każda funkcja tworzy taki stos na samym początku. Wszystkie zmienne definiowane wewnątrz funkcji są obiektami chwilowymi, a zatem są umieszczane na stosie. Podobnie jest z argumentami wysłanymi do funkcji. Również są one tworzone na stosie. Na chwilę przed zakończeniem funkcji cały stos jest zwalniany, a tym samym wszystkie dane ze stosu są usuwane. Właściwie wszystko to jest wykonywane automatycznie i wcale nawet nie musisz o tym wiedzieć. Jednak postanowiłem o tym powiedzieć, aby przybliżyć nieco działanie funkcji i to co za chwilę powiem. Przytoczę taki przykład. W funkcji jest definicja zmiennej. Jest ona inicjalizowana wartością 10. Podczas pracy funkcji wartość ta ulega zmianie i na koniec ma wynosi 20. Kończy się funkcja i wszystko ze stosu jest usuwane. Więc nasza zmienna również. Wchodzimy do funkcji i zmienna ma wartość znowu 10. Pomimo, iż przy ostatnim obiegu było 20. Zazwyczaj to nie przeszkadza, ale nie zawsze. Czasami przydałoby się aby zmienna zdefiniowana wewnątrz funkcji była nieśmiertelna. Można to zrobić i nawet wiesz jak. Tylko nie zdajesz sobie z tego sprawy 🙂 Przypomnij sobie niedawno omawiane słówko static. Przy jego omawianiu powiedziałem, że działanie jest uzależnione od zakresu ważności. Podtrzymuję to nadal. Tutaj mamy zakres funkcji i działanie słówka static jest całkowicie różne niż przedtem. Zmienne zdefiniowane z tym modyfikatorem w ciele funkcji są umieszczane po za stosem. Można powiedzieć, że istnieją przez cały czas trwania programu. Jednak są widoczne tylko w funkcji, w której zostały zdefiniowane.
1 2 3 4 5 6 7 8 9 |
void funkcja() { static int licznik; licznik++; } |
W tej funkcji użyliśmy zmiennej statycznej o nazwie licznik. Oznacza to, że licznik będzie się zwiększał z każdym wywołaniem funkcji. Początkowo będzie miał wartość zero. Jest to dość istotne. Zwykłe zmienne po utworzeniu zawierają przypadkowe dane. Zmienne statyczne są zawsze inicjalizowane zerem. Takie obiekty są przydatne w sytuacjach, gdy funkcja wykonuje jakieś operacje niezależnie od reszty programu. Warto o tym pamiętać. Teraz powiem jeszcze, jak można nieco przyspieszyć wywoływanie funkcji.
funkcje inline
Czasem zdarza się, że funkcja jest tak banalna, iż zajmuje jedną czy dwie linijki. Wtedy można umieścić ciało takiej funkcji bezpośrednio w miejscu jej wywołania. Chodzi tutaj o funkcje inline. Słówko inline, oznacza w linii i można je zastosować w bardzo krótkich funkcjach. Wówczas w miejscu wywołania funkcji zostanie wstawione całe ciało takiej funkcji. Mamy prostą funkcję szyfrującą:
1 2 3 4 5 6 7 |
void funkcja(int &argument) { argument = argument ^ 34; } |
Jak widać w funkcji zastosowaliśmy referencję. Jej działanie jest proste. Wewnątrz wykonuje się zwykła operacja logiczna na bitach. O tym już mówiłem przy operatorach. Dla nas najistotniejsze jest to, że funkcja jest bardzo krótka. W programie występują tysiące jej wywołań. Zatem doskonale nadaje się do naszego przykładu. Aby program działał lepiej można z tej funkcji zrobić funkcję inline. Definicja wygląda następująco:
1 2 3 4 5 6 7 |
inline void funkcja(int &liczba) { liczba = liczba ^ 34; } |
Od teraz wszelkie wywołania tej funkcji zostaną zamienione na linijkę będącą jej ciałem. Jest to przydatne w przypadku bardzo krótkich funkcji. Jeżeli funkcja zajmuje więcej niż dwie, trzy linijki to stanowczo odradzam stosowanie słówka inline. W takim przypadku może to wpłynąć niekorzystnie na działanie programu.
przeładowanie nazwy funkcji
Jeżeli programowałeś już w jakimś języku i korzystałeś z funkcji to na pewno docenisz to, o czym teraz powiem. Przeładowanie nazwy funkcji to bardzo ciekawa cecha C++. Dzięki temu w programie w tym samym zakresie może istnieć dowolna liczba funkcji posiadających tą samą nazwę! Do tej pory takie przekręty były niemożliwe. C++ wprowadził taki trik. Przypuśćmy, że masz funkcję do obliczania pola powierzchni wybranych figur. Oczywiście inaczej będzie wyglądać funkcja licząca pole koła, a inaczej trójkąta. Pisanie kilku funkcji o różnych nazwach dla poszczególnych figur jest trochę męczące. Zamiast nazywać funkcje tak: licz_pole_kola, licz_pole_trapezu, licz_pole_trójkąta itd. wygodniej jest napisać licz_pole. C++ dopuszcza taką możliwość. Jednak jak zawsze w takich sytuacjach zyskujemy coś kosztem czegoś innego 🙁 Zatrzymajmy się na moment. Jeżeli w jednym zakresie istnieje kilka funkcji o tych samych nazwach to należy je jakoś odróżniać. Pytanie tylko, jak to zrobić? Ano bardzo prosto. Wystarczy zadbać o unikalność argumentów każdej z funkcji. Chodzi o to, aby typy argumentów były różne w każdej z funkcji. Wystarczy nawet zamienić kolejność i błędu nie będzie.
1 2 3 |
void funkcja1(char znak, int liczba); void funkcja1(int liczba, char znak); |
Funkcje wyglądają podobnie. Mają jednakowe nazwy. Mimo to kompilator na to zezwala. To jest właśnie przeładowanie nazwy funkcji. Oczywiście takich funkcji może być więcej. Pamiętaj tylko o różnorodności typów argumentów. Czasem będą różne typy, czasem zamieniona kolejność [jak w przykładzie], a niekiedy różna ilość. Tylko uważaj tutaj! Podczas przeładowania nazwy funkcji w grę wchodzą jedynie typy argumentów wywołania. Typ zwracany nie jest brany pod uwagę. Zatem zmiana typu rezultatu nic nie da. Teraz powiemy sobie o domniemanych argumentach funkcji.
domniemane argumenty funkcji
Do tej pory podczas wywoływania funkcji należało podać dokładnie tyle argumentów, ile zakładała definicja samej funkcji. Jeżeli ich liczba była niezgodna to kompilator protestował. Okazuje się, że w funkcjach można umieszczać argumenty domniemane. Może już się domyślasz na czym to polega? Jeśli nie, to wyjaśniam. Argument domniemany to argument, który może zostać podany w wywołaniu funkcji lub nie. Weźmy taką sytuację. Funkcja służy do odczytu temperatury z urządzenia zewnętrznego podłączonego do komputera. Zazwyczaj temperatura jest podawana w stopniach Celsjusza. Jednak czasem chcemy mieś inną skalę. Rodzaj jednostki ustalamy w wywołaniu funkcji za pomocą numerków. Numer 0 to stopnie Celsjusza, a 1 Fahrenheita. Jak powiedziałem przeważnie jest to ta pierwsza skala. Wygodnie byłoby mieć funkcje, która w domyśle podaje skalę w stopniach Celsjusza, a jedynie na specjalne życzenie w stopniach Fahrenheita. Jest to możliwe za pomocą argumentów domniemanych. Jeżeli nie podasz nic zostanie wykorzystana skala w stopniach Celsjusza, w przeciwnym przypadku w stopniach Fahrenheita. Zerknij na przykład:
1 2 3 4 5 6 7 8 9 |
int podaj_temperature(int skala = 0) { if(skala == 0) //instrukcje odczytujące temperaturę w C else //instrukcje odczytujące temperaturę w F } |
Taką funkcję można wywołać dwojako. Pierwszy sposób to normalne wywołanie z określeniem konkretnej wartości argumentu skala.
1 |
podaj_temperature(0); |
To znamy od dawna. Drugi sposób jest ciut krótszy. Wystarczy napisać nazwę funkcji i parę nawiasów.
1 |
podaj_temperature(); |
Jak widzisz jest to pewne ułatwienie. Praktyka stosowania argumentów domniemanych we funkcjach jest dość często spotykana. Jednakże tutaj też trzeba uważać. Wszelkie argumenty domniemane trzeba umieszczać na końcu. Nie może być sytuacji, że pierwszy argument będzie domniemany, a pozostałe nie. Zastanów się chwilę, dlaczego jest to błąd. Aby to zrozumieć zerknij sobie na przykład:
1 |
void funkcja(int arg1 = 0, int arg2, int arg3 = 1); |
Jak myślisz, w jaki sposób można wywołać taką funkcję? Nie da się tego zrobić. Jeżeli w wywołaniu podasz jeden argument to można jeszcze się domyślić. Jednak co w sytuacji, gdy podasz dwa argumenty? Występują dwie możliwości. Funkcja zostanie wywołana z pierwszym argumentem domniemanym równym 0, drugi i trzeci to wartości podane w wywołaniu. Równie dobrze pasuje tutaj taka sytuacja. Pierwszy i drugi argument określimy w wywołaniu, a trzeci jako że jest domniemany będzie równy 1. Dlatego właśnie taka funkcja nie może zostać wywołana. Kompilator też to dostrzega i wyświetla stosowny komunikat.
Autor: Kesay
[Artykuł pochodzi ze strony guidecpp.prv.pl, autor wyraził zgodę na publikację]