Korzystając z przypływu weny pisarskiej, a nie tej ścisło naukowej - wykorzystam ten dzień na napisanie kolejnego rozdziału niniejszego kursu C++. W obecnym rozdziale omówione zostaną funkcje w zakresie podstawowym, dzięki którym będziesz mógł pisać kod lepszy, czytelniejszy i przede wszystkim łatwiejszy w utrzymaniu. Czy tak będzie w praktyce? To już zależy tylko od Ciebie, tj. od tego z jakim zaangażowaniem do obecnie czytanego rozdziału podejdziesz.
Co to są funkcje i argumenty
Skoro wstęp już mamy za sobą wyjaśnijmy sobie na początek czym są funkcje w językach programowania takich jak C i C++. Funkcja jest to fragment programu, któremu nadano nazwę i który możemy wykonać poprzez podanie jego nazwy oraz ewentualnych argumentów (o ile istnieją). Argumentami są natomiast dane przekazywane do funkcji.
Budowa funkcji
Zobaczmy teraz jak w teorii zbudowana jest funkcja:
typ_zwracanej_wartosci nazwa_funkcji( typ_argumentu_1 nazwa_argumentu_1 , typ_argumentu_n nazwa_argumentu_n )
{
return zwracana_wartosc;
}
W kursie, który pisałem kilka lat temu, zawarłem następujące stwierdzenia:
Każda funkcja posiada trzy własności:
|
Siłą rzeczy w tej kwestii nic się nie zmieniło od tamtego czasu, więc powyższe stwierdzenia są nadal jak najbardziej aktualne i właściwe.
Funkcja, która nie przyjmuje argumentów i nie zwraca wartości
Zanim przejdę do dalszej teorii - zatrzymajmy się w tym miejscu na chwilę i skupmy się na praktycznej części Twojej edukacji. Na początek zajmijmy się funkcją, która nie zwraca żadnej wartości, ani nie przyjmuje żadnych argumentów:
void to_jest_moja_funkcja()
{
}
Tak wygląda najprostsza funkcja w językach C i C++. Słowo kluczowe
void informuje kompilator (jak również i programistę), że funkcja nie zwraca żadnej wartości. Nazwą funkcji w tym przypadku jest
to_jest_moja_funkcja. Funkcja nie przyjmuje żadnych argumentów, bowiem wewnątrz zaokrąglonych nawiasów jest pusto. Wewnątrz klamr umieszczamy kod, który ma się wykonać w chwili gdy zostanie wywołana funkcja. Jest to blok instrukcji, który zwie się ciałem funkcji.
Wywoływanie funkcji
Wywoływanie funkcji jest bardzo proste - wystarczy wpisać jej nazwę i przekazać wartości do funkcji. Ogólna postać wywołania funkcji wygląda następująco:
nazwa_funkcji( wartosc_argumentu_1 , wartosc_argumentu_n );
Jak widać używanie funkcji jest banalne.
Przykład funkcji, które nie przyjmują argumentów i nie zwracają wartości
Teoria, teorią ale zobaczmy jak to wygląda w praktyce.
#include <iostream>
void moja_funkcja()
{
std::cout << "[1] - dodawanie" << std::endl;
std::cout << "[2] - odejmowanie" << std::endl;
std::cout << "[0] - wyjscie z programu" << std::endl;
}
void dodawanie()
{
std::cout << "Jeszcze nie oprogramowano" << std::endl;
}
void odejmowanie()
{
dodawanie();
}
int main()
{
std::cout << "W programie sa dostepne nastepujace opcje:" << std::endl;
moja_funkcja();
std::cout << "Zycze przyjemnego korzystania z programu" << std::endl << std::endl;
int liczba;
do
{
moja_funkcja();
std::cin >> liczba;
switch( liczba )
{
case 1:
dodawanie();
break;
case 2:
odejmowanie();
break;
default:
break;
}
} while( liczba != 0 );
return 0;
}
Przeanalizuj na spokojnie powyższy program, przetestuj jego działanie i zastanów się czy aby na pewno wszystko rozumiesz i jest dla Ciebie jasne w 100%. Jeżeli nie - zrób sobie krótką przerwę na herbatę, po czym wróć do ponownej analizy programu - krótkie przerwy dobrze służą programistom.
Komentarz do przykładu
Jak widać program jest już trochę dłuższy niż te, które do tej pory pojawiały się w przykładach. Musisz wiedzieć, że kod będzie bardzo szybko rozrastał się w każdej pisanej przez Ciebie aplikacji. Funkcje należą do narzędzi, które umożliwiają Ci podzielenie kodu aplikacji na mniejsze części. Tworzenie funkcji jest najpotężniejszą bronią w ręku początkującego programisty, które jest niestety przez nich samych nie doceniane i z uporem maniaka omijane szerokim łukiem - a to błąd. Dzięki funkcjom możesz wydzielić funkcjonalność np. odpowiedzialną za dodawanie i odejmowanie do osobnych bloków, które są po pierwsze nazwane, a po drugie krótsze, bowiem ciało funkcji realizuje tylko fragment całego programu. Nazwa funkcji powinna mówić programiście za co funkcja jest odpowiedzialna - dzięki takiemu podejściu wracając do kodu po miesiącu albo nawet podczas szukania błędu nie będziesz musiał skakać po całym kodzie, tylko po fragmencie, który jest odpowiedzialny np. za dodawanie.
Ćwiczenie praktyczne
Dokończ wyżej przedstawiony program - popraw funkcje
dodawanie i
odejmowanie tak, aby pierwsza z nich sumowała dwie podane liczby i wypisywała wynik, a druga odejmowała dwie liczby i wypisywała wynik. Po ukończeniu programu zastanów się jak by wyglądał kod gdybyś nie skorzystał z funkcji. Zastanów się czy faktycznie łatwiej będzie znaleźć ewentualne błędy w kodzie tworzonym w oparciu o funkcje.
Zmienne wewnątrz funkcji, czyli zasięg widzenia zmiennych
Jeżeli wykonałeś ćwiczenie praktyczne z niniejszego rozdziału to zapewne nieświadomy niczego utworzyłeś dwie zmienne wewnątrz funkcji
dodawanie oraz dwie zmienne wewnątrz funkcji
odejmowanie. Jeżeli tego nie zrobiłeś to znaczy, że albo nie wykonałeś zadania albo wykonałeś je źle, bowiem użyłeś zmiennych globalnych o których nie powinieneś nic wiedzieć. Jeżeli użyłeś zmiennych globalnych to znaczy, że albo użyłeś Internetu w sposób niewłaściwy albo jakiś 'mądry' kolega doradził Ci najgorsze możliwe rozwiązanie ze zmiennymi globalnymi. Jeżeli sam wpadłeś na niepoprawne rozwiązanie - to da się wybaczyć, ale raczej jest to mało prawdopodobne jeżeli uczysz się programowania od zera z niniejszego kursu. Przytoczmy najpierw poprawnie napisaną funkcję
dodawanie:
void dodawanie()
{
int a;
std::cin >> a;
int b;
std::cin >> b;
std::cout << "Wynik dodawania " << a << " + " << b << " = " << a + b << std::endl;
}
Na chwilę obecną nie jest istotny fakt, że nie zabezpieczyliśmy funkcji przed możliwością wpisania nieprawidłowych danych - ważny jest tu fakt zadeklarowania zmiennych
a oraz
b typu
int. Zmienne, które utworzyliśmy wewnątrz funkcji są zmiennymi lokalnymi. Oznacza to, że nie są one widoczne poza funkcją i istnieją one tylko w obrębie danej funkcji. W praktyce oznacza to, że z funkcji głównej
main nie będziesz miał dostępu do zmiennych
a i
b. Próba uzyskania dostępu do zmiennej
a z funkcji
main zakończy się następującym błędem:
In function 'int main()':|
error: 'a' was not declared in this scope|
Komunikat ten mówi:
W funkcji 'int main()': błąd: 'a' nie zostało zadeklarowane w zasięgu.". Innymi słowy zmienna o wyszczególnionej nazwie nie istnieje w ciele funkcji
main. Linijka, która spowodowała błąd wyglądała następująco:
Analogiczny błąd uzyskamy, gdy z funkcji
dodawanie spróbujemy dostać się do zmiennej
liczba, która występuje w ciele funkcji
main. Wniosek z tego płynie prosty: funkcje nie widzą swoich zmiennych nawzajem. Bardzo piękna własność funkcji, którą Ty jeszcze będziesz nie raz przeklinał :)
Czas życia zmiennych w funkcjach
Zmienne, które zostały utworzone w funkcji są zmiennymi tymczasowymi. Zmienne te pojawiają się do użytku przy każdym wywołaniu funkcji jak również znikają po jej opuszczeniu. Zmienne te nie trzymają stanu z poprzedniego wywołania - innymi słowy za każdym razem po wejściu w funkcję musisz nadać wartości zmiennym na nowo.
Zachowanie zmiennych lokalnych można zmienić tak, by przy każdym wejściu w funkcję była to ta sama zmienna i zachowywała swoją ostatnią wartość. Bardzo rzadko się to jednak stosuje i jako początkujący programista nie powinieneś tych technik używać - najpierw musisz bowiem dobrze zrozumieć podstawy by móc efektywnie programować.
Komunikowanie się między funkcjami
Skoro już wiemy, że zmienne funkcji są od siebie całkowicie odizolowane fajnie by było gdyby mimo wszystko funkcje mogły się ze sobą jakoś wymieniać się informacjami. Pierwszą metodą są zmienne globalne - zło w czystej postaci - za to powinni palić na stosie początkujących programistów :) Kiedyś niestety będę musiał napisać co to jest, jednak prędko to nie nastąpi, więc mam nadzieję, że zdążysz się nauczyć programować poprawnie do tego czasu.
Zwracanie wartości przez funkcję
Drugą metodą jak już się domyślasz jest zwracanie wartości przez funkcję. Przykładem takiej funkcji była np. funkcja
rand. Funkcja ta pełniła rolę czarnej skrzynki - w jakiś cudowny sposób wyliczyła wartość losową, a następnie zwróciła nam wynik, który mogliśmy później dowolnie przetwarzać. Jak zerkniesz na budowę funkcji, która została wyżej opisana zobaczysz, że na samym początku znajduje się w zapisie stwierdzenie
typ_zwracanej_wartosci. Innymi słowy za ten zapis podstawiasz typ danych jaki ma zwrócić funkcja. Na chwilę obecną niech to będzie liczba zmiennoprzecinkowa
float. Przykład:
float dodawanie_inne()
{
float a;
std::cin >> a;
float b;
std::cin >> b;
return a + b;
}
W powyższej funkcji występuje również słowo kluczowe
return. Słowem kluczowym
return ustawiamy wartość jaka ma zostać zwrócona przez funkcję. Myślę, że niczego trudnego tutaj nie ma, więc możemy się teraz skupić na tym w jaki sposób odczytywać zwracane wartości.
Odczytywanie wartości zwracanej przez funkcję
Funkcja zwraca wartość, czyli poniższe zapisy przekażą nam wartość do tego co będzie występowało po lewej stronie funkcji (łopatologiczne wytłumaczenie, ale co tam):
float wynik = dodawanie_inne();
wynik = dodawanie_inne();
std::cout << dodawanie_inne();
dodawanie_inne();
W ostatnim przypadku po lewej stronie funkcji nie ma nic - oznacza to tyle, że funkcja się wykona i zwróci wartość ale nie zostanie ona nigdzie zapisana. W konsekwencji wynik obliczeń przepadnie - czy to będzie pożądane? To zależy od tego co funkcja będzie robiła. Jeżeli funkcja załóżmy zwraca informację o tym czy funkcja się powiodła czy nie, a Ty niespecjalnie się tym przejmujesz zakładając, że zawsze zadziała to i zwrot funkcji na nic Ci nie będzie potrzebny. Dobry programista jednak odnosi się z szacunkiem do informacji zwracanych przez funkcje i zastanowi się chociaż czy przypadkiem nie powinien on oprogramować sytuacji wyjątkowych, takich jak np. błąd działania funkcji. Wywody, wywodami no ale wróćmy do nauki. Mamy teraz następujący kod:
#include <iostream>
float dodawanie_inne()
{
float a;
std::cin >> a;
float b;
std::cin >> b;
return a + b;
}
int main()
{
std::cout << "Wprowadz dwie liczby: ";
float tu_bedzie_wynik = dodawanie_inne();
std::cout << "Wynik dodawania wynosi: " << tu_bedzie_wynik << std::endl;
return 0;
}
Jeżeli przeanalizujesz dobrze powyższy kod to zauważysz, że uzyskaliśmy teraz komunikację jednostronną między funkcjami. Funkcja
main otrzymuje informacje zwrotną, od funkcji
dodawanie_inne. Informacja, która została zwrócona została przygotowana przez funkcję
dodawanie_inne i mówiąc potocznie 'wystawiona' do odczytania. Zauważmy jednak, że nadal nie możemy komunikować się w drugą stronę, tj. przekazywać informacji z funkcji
main do funkcji
dodawanie_inne. Z pomocą przyjdą tutaj argumenty funkcji, które jeszcze nie zostały omówione.
Przekazywanie wartości do funkcji poprzez argumenty
Trzecią techniką komunikacji między funkcjami jest zastosowanie argumentów funkcji. Argumenty umożliwiają komunikację w dwie strony, jednak w tym rozdziale zajmiemy się tylko i wyłącznie komunikacją w jedną stronę, tj. przekazywaniu danych do funkcji. Dlaczego? Bo tak :) Najlepszym uzasadnieniem, które powinno do Ciebie trafić jest fakt, że wiele początkujących osób totalnie nie rozumie co to jest argument, jak działa i jakie mają znaczenie poszczególne znaczki. Dlatego też zaczniemy od podstaw, które trzeba opanować by wiedzieć później po co się robi różne inne cuda o których z pewnością będę pisał w dalszej części kursu.
Powróćmy teraz na chwilę do teorii, która została przedstawiona na samym początku niniejszego rozdziału:
typ_zwracanej_wartosci nazwa_funkcji( typ_argumentu_1 nazwa_argumentu_1 , typ_argumentu_n nazwa_argumentu_n )
{
return zwracana_wartosc;
}
Jedynym zagadnieniem, które nie zostało omówione do tej pory są zapisy występujące pomiędzy nawiasami zaokrąglonymi. To co znajduje się między nimi nazywamy argumentami. Każdy argument musi mieć określony swój typ. Dodatkowo po nazwie typu podaje się również nazwę zmiennej po to by można było się dostać do argumentu naszej funkcji. Kolejne argumenty funkcji rodziela się
przecinkami. Zobaczmy teraz kolejną funkcję, która będzie wykonywała operację dodawania:
int dodawanie( int a, int b )
{
return a + b;
}
Wywołanie tej funkcji natomiast będzie wyglądało tak:
int iWynik = dodawanie( 123, 456 );
std::cout << "Wynik dodawania wynosi: " << iWynik << std::endl;
Powyższe zapisy spowodują, że:
Choć trochę śmiesznie wygląda funkcja, która wykonuje tak prostą operację to mimo wszystko pokazuje ona w jaki sposób argumenty funkcji działają i jak ustawiać im wartości.
Zadanie domowe