Funkcje

   Czasami trzeba w programie powtórzyć jakieś czynności kilkakrotnie. Na przykład chcemy narysować na ekranie kilkanaście jednakowych domków. Jednak umieszczenie kodu rysującego pojedynczy domek w pętli nie wchodzi w grę, bo my chcemy te domki rysować w dość przypadkowych miejscach ekranu, w dodatku chcemy, żeby każdy z domków miał dach w innym kolorze. Jak to rozwiązać? Do takich zadań służą w C++ funkcje. Używaliśmy już funkcji, nawet się nad tym nie zastanawiając. Przecież cały nasz program był jedną wielką funkcją, która nazywała się main!

   Funkcja to jakby zastąpienie jedną instrukcją wielu instrukcji wykonujących jakieś konkretne zadanie. W naszym przypadku rysowanie domków. Ponieważ narazie zajmujemy się konsolą, w której raczej trudno narysować domek, zajmiemy się czymś innym. Na przykład... odpalaniem rakiet nuklearnych ;-D. A ponieważ w C++ nie ma polecenia odpalającego rakiety [no bo z jakiej racji miałoby być ;-P], napiszemy je sobie sami! :-) Wpisz przed funkcją main coś takiego:
void OdpalRakiety() { //To jest ciało funkcji, czyli instrukcje, z których funkcja się składa } To jest definicja funkcji, czyli określenie, co funkcja ma robić [jakie zawiera instrukcje]. Przyjrzyjmy się dokładniej tej konstrukcji. Nasza nowa funkcja będzie się nazywać OdpalRakiety. W nawiasach klamrowych, czyli w ciele funkcji [tam gdzie jest ten komentarz], będą się znajdować wszystkie instrukcje, które chcemy wykonywać przy pomocy jednego polecenia. Na razie nasza funkcja będzie tylko wypisywać komunikat, że rakiety "poszły" ;-). Wywal więc komentarz i wstaw taki kod:
void OdpalRakiety() { cout << "Ustawiam wyrzutnie\n"; cout << "Rakiety zostaly odpalone i leca... gdzies...\n"; } Teraz wystarczy, że w funkcji main wywołamy naszą funkcję, czyli wydamy jedno krótkie polecenie:
OdpalRakiety(); a program "ustawi wyrzutnię rakiet" ;-), po czym odpali rakiety. Zrobimy teraz mały eksperyment. Wytnij cały kod naszej funkcji sprzed funkcji main i wklej go za nią. Skompiluj program i zobacz co się stanie. Jak widzisz, kompilator wywalił błąd. Dlaczego?

Zapowiedź funkcji

   Zamierzamy użyć nowego polecenia w C++, czyli swojej funkcji. Kompilator musi ją więc znać, żeby mógł dobrze wykonać swoją pracę. W pierwszym przypadku zdefiniowaliśmy całą funkcję jeszcze przed funkcją main. Gdy więc kompilator analizuje funkcję main i napotka na wywołanie naszej funkcji OdpalRakiety, to już ją zna. W drugim przypadku jest inaczej. Kompilator dochodząc do takiego wywołania nie zna jeszcze wogóle naszej funkcji OdpalRakiety! Dlatego wywala błąd [jeśli czytasz książkę i jesteś w połowie, to nie znasz jeszcze zakończenia ;-)]. Wygodniej i czytelniej jest definiować swoje funkcje poniżej funkcji main, więc jeśli robisz to w taki sposób, to wypadałoby chociaż jakoś powiadomić kompilator, że masz zamiar użyć swojej funkcji i będzie się ona nazywać tak a tak. Robi się to w ten sposób, że przed funkcją main piszesz tylko zapowiedź tej funkcji [bez określania jej ciała], a zdefiniować ją sobie możesz gdzie chcesz. Jedynym miejscem, gdzie nie wolno definiować funkcji, jest ciało innej funkcji. A tak wyglądałaby zapowiedź naszej funkcji:
void OdpalRakiety(); A co, jeśli potrzebujesz wywołać funkcję, która znajduje się w innym pliku? Na przykład masz napisany zestaw funkcji do rysowania na ekranie. Wszystkie te funkcje znajdują się w pliku biblioteka.cpp. Chcesz teraz użyć jednej z nich w funkcji main, która znajduje się w pliku program.cpp. Jak to zrobić? Wyjścia są conajmniej dwa. Po pierwsze możesz w pliku program.cpp napisać zapowiedź tej funkcji, której chcesz użyć. Ale wyobraź sobie, że jest ponad 10 plików i w każdym z nich chcesz użyć po kilka funkcji z pliku biblioteka.cpp. Czy naprawdę masz zamiar pisać zapowiedzi tych funkcji w każdym z tych dziesięciu plików?! :-@ Istnieje prostszy sposób. Wystarczy, że napiszesz tą zapowiedź tylko raz, w dodatkowym pliku biblioteka.h, po czym dołączysz ten plik do każdego z pozostałych dyrektywą #include "biblioteka.h". Robiliśmy tak już nieraz, dołączając pliki nagłówkowe bibliotek. Jeśli otworzysz plik nagłówkowy jakiejś biblioteki to zobaczysz, że zawiera on właśnie zapowiedzi wszystkich funkcji, które są zawarte w danej bibliotece :-). Prawda, że prościej jest to robić w taki sposób? Zwłaszcza, jeśli funkcji ma być dużo. Widzisz teraz, do czego mogą się przydać zapowiedzi funkcji, oraz dzielenie programu na kilka plików [choć to nie jedyny powód ;-)].

Parametry funkcji

   Pozwól że zadam ci jedno małe pytanko. Napisaliśmy przed chwilą funkcję, która odpala rakiety, no nie? No to mi teraz powiedz, na jaki cel te rakiety lecą??? :-J No właśnie... Nie wiesz... Przez nasz brak precyzji rakiety poleciały właśnie na przykład do Rusków i rozpętaliśmy III wojnę światową ;-D. Dolas byłby z nas dumny! ;-D

   Ale nie bój nic! Widzisz tą parę nawiasów przy nazwie funkcji? No więc są tam one nie od parady. Możesz umieścić między nimi jakąś informację dla funkcji, którą właśnie wywołujesz. Takie coś nazywa się parametrem funkcji. My chcielibyśmy jakoś podać funkcji szerokość i długość geograficzną celu, który chcemy zrównać z ziemią. Na przykład tak:
OdpalRakiety(21,52); I teraz funkcja, zależnie od tych współrzędnych, mogłaby nam wyświetlić nazwę celu. Współrzędne celu niech będą dla ułatwienia typu int. Powiedzmy więc kompilatorowi, że nasza funkcja będzie pobierać dwa parametry typu int:
void OdpalRakiety(int dlugosc, int szerokosc) { cout << "Ustawiam wyrzutnie\n"; cout << "Cel: "; if (dlugosc==21 && szerokosc==52) cout << "Warszawa\n"; if (dlugosc==37 && szerokosc==56) cout << "Moskwa\n"; else cout << "nieznany obiekt\n"; cout << "Odpalam rakiety!\n"; } Jak widzisz, podaliśmy w nawiasach listę kolejnych parametrów [wraz z ich typami], oddzielonych przecinkiem. Pewnie powiesz, że przypomina ci to deklarowanie zmiennych. Więc powiem ci, że jesteś bardzo blisko! :-) Bo parametry są niczym innym, jak zmiennymi jakiegoś typu, które wrzucamy do funkcji. Kompilator widząc taki kod zarezerwuje wewnątrz funkcji dwie zmienne typu int w specjalnym miejscu pamięci zwanym stosem. Pierwsza zmienna będzie się nazywać dlugosc, a druga szerokosc. Pierwszej z nich przypisze wartość pierwszego parametru, a drugiej - drugiego. Zmienne takie będą w dodatku wyłącznie własnością funkcji! Podobnie zresztą z innymi zmiennymi zadeklarowanymi wewnątrz funkcji. Na takie zmienne mówimy, że są lokalne dla tej funkcji. Gdy funkcja się zakończy, kompilator automatycznie usunie te zmienne z pamięci stosu. Zmienna lokalna ma jeszcze jedną zaletę: na jej nazwę nie ma wpływu to, czy gdzieś poza funkcją jest już zmienna o takiej samej nazwie. Po prostu dla funkcji bliższe są zmienne, które są jej własnością. Jeśli poza funkcją jest zmienna, która nazywa się tak samo jak zmienna lokalna, to zostanie jakby "zasłonięta" przez tą zmienną lokalną.

   Zapowiedź powyższej funkcji możesz zapisać na dwa sposoby. W pierwszym podajesz "nagłówek" funkcji, wraz z całą listą parametrów. Ale zauważ, że w zapowiedzi kompilatorowi i tak nie jest na nic potrzebne znać nazwy parametrów. Przecież to już jest sprawa wewnętrzna funkcji. Nazwy są konieczne tylko w definicji funkcji, natomiast w zapowiedzi wystarczy podać typy parametrów. I to jest właśnie sposób drugi, prostszy. Poniżej masz oba sposoby:
void OdpalRakiety(int dlugosc, int szerokosc); void OdpalRakiety(int, int); Czasami programiści korzystają jednak ze sposobu pierwszego, na przykład gdy piszą bibliotekę dla innych programistów. Robią to po to, że wtedy lepiej można się zorientować, jakie parametry pobiera funkcja i do czego one jej są potrzebne.

Wyjście z funkcji

   Skoro możemy już podać naszej funkcji współrzędne celu, to dobrze by było zabezpieczyć się przed możliwością wysadzenia w powietrze samego siebie ;-P. Do zakończenia działania funkcji i opuszczenia jej służy instrukcja return. Gdy wewnątrz funkcji odkryjemy, że mamy zamiar posłać rakiety na własny kraj, użyjemy słówka return by wyjść z funkcji. Look:
void OdpalRakiety(int dlugosc, int szerokosc) { cout << "Ustawiam wyrzutnie\n"; cout << "Cel: "; if (dlugosc==21 && szerokosc==52) { cout << "Warszawa! - Nic z tego!\n"; return; } if (dlugosc==37 && szerokosc==56) cout << "Moskwa\n"; else cout << "nieznany obiekt\n"; cout << "Odpalam rakiety!\n"; } Ale słówko return ma jeszcze jedną przydatną właściwość. Dzięki niemu możemy przy wychodzeniu z funkcji zwrócić do programu jakieś dane. Skorzystajmy teraz z tego dobrodziejstwa. Niech nasza funkcja OdpalRakiety zwraca wartość logiczną mówiącą, czy odpalenie rakiet się powiodło. Zobacz, jak to napisać w C++ :
bool OdpalRakiety(int dlugosc, int szerokosc) { cout << "Ustawiam wyrzutnie\n"; cout << "Cel: "; if (dlugosc==21 && szerokosc==52) { cout << "Warszawa! - Nic z tego!\n"; return false; } if (dlugosc==37 && szerokosc==56) cout << "Moskwa\n"; else cout << "nieznany obiekt\n"; cout << "Odpalam rakiety!\n"; return true; } Co się zmieniło? Zamieniliśmy w nagłówku funkcji słówko void na słówko bool. Co to oznacza? Poprzednio funkcja nie zwracała żadnej wartości do kodu, który ją wywołał. Czyli typ wartości zwracanej przez funkcję był void [żaden]. Teraz funkcja zwraca wartość logiczną, czyli typ wartości zwracanej jest teraz bool. Taki zapis wziął się z matematyki. W matematyce funkcję zapisuje się jako y = f(x), gdzie f to nazwa funkcji, x to parametr, a y to wartość zwracana. W C++ jest tak samo, więc łatwo to zapamiętać :-). A do zwrócenia wartości służy, jak już wspomniałem, instrukcja return. W naszym przykładzie zwracamy false gdy rakiety nie zostały odpalone, a true, gdy odpalamy rakiety. Co nam to daje? Na przykład możemy teraz wywołać naszą funkcję w taki sposób:
if ( OdpalRakiety(21,52) ) cout << "Rakiety zostaly odpalone :>\n"; else cout << "Nie odpalono rakiet :(\n"; Jeśli teraz wszystko będzie OK, funkcja zwróci true i wykona się instrukcja z pierwszej linijki. Natomiast jeśli funkcja zwróci false, to wykona się druga linijka. A tak wygląda teraz zapowiedź naszej funkcji:
bool OdpalRakiety(int,int); W takim zapisie widać jak na dłoni, co wchodzi do funkcji, a co z niej wychodzi. Wartość zwracana przez funkcję może być zmienną dowolnego typu, oprócz tablicy. Da się to jednak obejść, zwracając zamiast tego wskaźnik do tablicy [do jej pierwszego elementu]. Podobnie jeśli chodzi o parametry przekazywane do funkcji.

Przekazywanie przez wartość

   Zostawmy na chwilę nasze rakiety, niszczenie świata może poczekać ;-). Zajmijmy się na moment jakąś prostszą funkcją. Będzie się ona nazywać Kwadrat i będzie służyć do podnoszenia liczb do kwadratu. X do kwadratu to nic innego jak x razy x. Nasza funkcja będzie więc zdefiniowana tak:
float Kwadrat(float x) { x = x*x; return x; } Teraz użyjemy tej funkcji. Zmienna liczba będzie zawierać liczbę, którą chcemy podnieść do kwadratu. Wynik, który zwróci funkcja, sypniemy na ekran. Dodatkowo wypiszemy jeszcze wartość zmiennej liczba po tej całej operacji.
void main() { float liczba = 2; cout << liczba << " podniesione do kwadratu daje " << Kwadrat(liczba) << endl; cout << "Po tym dzialaniu liczba=" << liczba << endl; } Skompiluj teraz program i zobacz, co się stało. Po wykonaniu działania wynik był oczywiście poprawny, ale po wyświetleniu się na ekranie...przepadł! :-o   Zmienna x ma dalej taką samą wartość. Dlaczego? Prosta sprawa. Przy przekazywaniu parametru do funkcji, kompilator tylko skopiował jego WARTOŚĆ i przypisał ją do zmiennej x lokalnej wewnątrz funkcji. Takie przekazywanie parametru nazywa się przekazywaniem przez wartość. Wewnątrz funkcji pracujemy więc tylko na kopii zmiennej podanej jako parametr, nie możemy modyfikować oryginału. W podobny sposób zwracany jest wynik. Kompilator kopiuje wartość zwracaną do tymczasowego obiektu typu float [którego oficjalnie nie ma i nigdzie go nie widać]. Po wypisaniu jego wartości na ekranie obiekt zostaje zlikwidowany i tyle go widzieli ;-). Ale my byśmy woleli pozostawić ten wynik przy życiu. Co więc zrobić, żeby funkcja Kwadrat zapisała wynik bezpośrednio w zmiennej liczba?

Przekazywanie przez referencję

   Instnieje inny sposób przekazywania argumentów do funkcji. I właśnie tutaj przychodzi nam z pomocą referencja. Jak pewnie pamiętasz, referencja to jakby inna nazwa [przezwisko] zmiennej. Jeśli parametr funkcji będzie przyjmowany jako referencja, to tak jakbyśmy posłali do funkcji zmienną we własnej osobie. Wewnątrz funkcji będziemy używać już nie kopii, ale samej zmiennej, tylko będzie ona mieć inną nazwę dzięki referencji :-). Po takiej zmianie:
float Kwadrat(float & x) { x = x*x; return x; } Funkcja będzie już modyfikować wartość oryginału, a nie tylko kopii jak poprzednio, dzięki czemu zmienna liczba będzie teraz podniesiona do kwadratu po wykonaniu funkcji. I to wszystko dzięki jednemu dodatkowemu znaczkowi ;-). Można także zwracać wynik funkcji poprzez referencję. Wtedy nagłówek funkcji wygląda tak:
float & Kwadrat(float & x) { x = x*x; return x; } W naszym przypadku oczywiście nic by to nie zmieniło, po prostu zamiast tymczasowej zmiennej typu float wypisywalibyśmy na ekranie referencję zmiennej x, czyli praktycznie to samo. Czasami jednak taki sposób zwracania wartości jest przydatny, pogadamy o tym przy okazji omawiania własnych operatorów. Wypadałoby jeszcze wspomnieć o jednej rzeczy. Trzeba uważać, żeby nie zwracać referencji czegoś, co już nie będzie istniało poza funkcją [na przykład jej zmiennych lokalnych].

Przekazywanie przez wskaźnik

   Trzeci sposób przekazywania parametrów do funkcji [lub zwracania wartości] polega na użyciu wskaźników. Pamiętasz jak mówiłem, że parametrem funkcji nie może być tablica? No to teraz poznasz sposób, w jaki MOŻNA tablicę wrzucić do funkcji. Dajmy na to, że masz taką tablicę znaków:
char Napis[] = "Cze, bestyjo! :>"; Jak możnaby ją przekazać do funkcji, która służy do wypisywania tekstu na ekranie? Przy okazji omawiania wskaźników mówiłem, że nazwa tablicy jest jakby wskaźnikiem do jej pierwszego elementu. Jeśli dałoby się wrzucić do funkcji taki wskaźnik, to jesteśmy w domu. :-) No to powiem ci, że da się! :-D Spójrz:
void Wypisz(char * text) { cout << text << endl; } Teraz już możemy posłać do funkcji tablicę, wiedząc, że jej nazwa jest adresem jej pierwszego elementu [wskaźnikiem na jej pierwszy element]. Popatrz:
Wypisz(Napis); W podobny sposób możemy teraz wypisać ten tekst zaczynając od innej litery. Wystarczy, że poślemy do funkcji wskaźnik na inny element tablicy [adres tego elementu]. Poniższa instrukcja:
Wypisz(&Napis[5]); Spowoduje wypisanie tylko "bestyjo! :>". Zauważ, że odwoływanie się do parametru poprzez jego wskaźnik również pozwala na modyfikowanie oryginału. Podobnie jak referencję, funkcja może także zwracać wskaźnik. I tutaj też trzeba uważać, żeby nie zwracać wkaźnika do obiektów, które przestaną istnieć po opuszczeniu funkcji. Pamiętaj o tym, bo tutaj bardzo łatwo o pomyłki i trudne do wykrycia błędy! :-|

Na tym zakończę tą już i tak za długą lekcję. Myślę, że nie zanudziłem cię tymi siedmioma ekranami tekstu ;-).