« Funkcje - pierwsze starcie, lekcja »
Rozdział 17. Co to są funkcje i jak się z nich korzysta. (lekcja)
Panel użytkownika
Nazwa użytkownika:
Hasło:
Nie masz jeszcze konta?
Zarejestruj się!
Autor: Piotr Szawdyński
Kurs C++

Funkcje - pierwsze starcie

[lekcja] Rozdział 17. Co to są funkcje i jak się z nich korzysta.
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:
C/C++
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:
  • zwraca dane (lub nie jeśli tego nie chcemy);
  • posiada swoją nazwę;
  • może posiadać dowolną liczbę argumentów wejściowych (lub może nie mieć żadnego, jeśli tego nie chcemy).
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:
C/C++
void to_jest_moja_funkcja()
{
    //INFO: tu kod Twojej funkcji
}
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:
C/C++
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.
C/C++
#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;
        } //switch
    } 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:
C/C++
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:
C/C++
a = 123;
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 » standard Crand. 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:
C/C++
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):
C/C++
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:
C/C++
#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:
C/C++
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:
C/C++
int dodawanie( int a, int b )
{
    return a + b;
}
Wywołanie tej funkcji natomiast będzie wyglądało tak:
C/C++
int iWynik = dodawanie( 123, 456 );
std::cout << "Wynik dodawania wynosi: " << iWynik << std::endl;
Powyższe zapisy spowodują, że:
  • Pierwszy argument funkcji (argument a) otrzyma wartość 123;
  • Drugi argument funkcji (argument b) otrzyma wartość 456;
  • Wynik funkcji dodawanie(...) zostanie zwrócony do zmiennej iWynik;
  • Wartość znajdująca się w zmiennej iWynik zostanie wypisana na ekran.
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

  • Napisz funkcję, która będzie wczytywała liczby ze standardowego wejścia i zwracała wczytaną wartość. Funkcja ma zagwarantować, że zwracana liczba zawsze jest poprawna. Oznacza to, że funkcja będzie musiała pytać użytkownika o wprowadzenie liczby dopóki nie poda poprawnej.
    C/C++
    #include <iostream>
    int wczytajLiczbe()
    {
        //TODO: tu Twój kod
    }

    int main()
    {
        std::cout << "Podaj liczbe: ";
        int liczba = wczytajLiczbe();
        std::cout << "Podales liczbe: " << liczba << std::endl;
        return 0;
    }
  • Zmodyfikuj kalkulator, który pisałeś w jednym ze wcześniejszych rozdziałów tak, aby korzystał on ze wcześniej napisanej funkcji wczytajLiczbe.
  • Napisz funkcję, która losuje liczbę z przedziału od 50 do 60 włącznie. Wywołaj funkcję kilka razy (wypisz wylosowane wartości na ekran) w celu przetestowania czy działa ona poprawnie.
  • Napisz funkcję, która losuje liczbę z przedziału, który zostanie podany poprzez argumenty funkcji. Przetestuj funkcję w poniższy sposób:
    C/C++
    #include <iostream>
    #include <cstdlib>
    #include <ctime>
    //... tu Twój kod
    int main()
    {
        srand( time( NULL ) );
        int start;
        std::cin >> start;
        int stop;
        std::cin >> stop;
        int ile = 20;
        do
        {
            std::cout << wylosuj( start, stop ) << std::endl;
            ile--;
        } while( ile > 0 );
       
        return 0;
    }
Poprzedni dokumentNastępny dokument
Pseudolosowe liczby całkowiteTablice jednowymiarowe