Funkcje w języku C++ - podstawowe informacje o funkcjach

Wprowadzenie

Do tej pory udało Ci się poznać już fragment języka C++. Umiesz przeprowadzać różne operacje, pobierać i wypisywać dane. Czas jednak poznać mechanizm, który ułatwia i tak naprawdę umożliwia programowanie.

Wyobraź sobie, że musisz napisać program, który będzie liczyć kilkadziesiąt tysięcy linii. Przerażające, prawda? Rzecz w tym, że mając tak duży program, na pewno wiele razy popełnisz bardziej lub mniej widoczne błędy, z których na pewno niektóre będą bardzo ciężkie do wykrycia.

Załóżmy, że udało Ci się program napisać, testowany był już wiele razy. Postanawiasz jednak przetestować go po raz ostatni przed zatwierdzeniem. I nagle okazuje się, że dla wprowadzonych danych, program nie zadziałał poprawnie - zawiesił się lub wypisał niepoprawne, absurdalne dane.

I co teraz zrobisz? Gdzie będziesz szukać błędu? Jak znaleźć błąd w programie, który liczy kilkadziesiąt tysięcy linii? Rzeczywiście, gdyby programy były pisane w ten sposób jak robiliśmy to do tej pory, to znalezienie takiego błędu byłoby niemożliwe albo graniczyłoby z cudem.

Na szczęście, poznamy teraz mechanizm, który ułatwia organizację kodu programu. Cały kod będziemy mogli dzielić na mniejsze części, które będzie można zdecydowanie łatwiej testować i łatwiej zrozumieć, co się w nich dzieje.

Tym tajemniczym mechanizmem będą funkcje, o których wspominałem już w lekcji 2, w której przedstawiłem ogólną budowę programu w C++.

Na omówienie funkcji poświęcę kilka najbliższych lekcji. Wiedz, że w najbliższych lekcjach, przy okazji omawiania funkcji powróci wiele zagadnień i mechanizmów omówionych w poprzednich lekcjach kursu. Jeśli zatem pojawią się sformułowania, których nie będziesz rozumieć, radzę Ci od razu wrócić do poprzednich lekcji, bowiem jeśli nie będziesz rozumieć tych zagadnień, tym bardziej nie uda Ci się zrozumieć jak działają funkcje.

Funkcje - budowa

Przyjrzymy się funkcji, której używamy począwszy od naszego pierwszego programu, czyli specjalnej funkcji - funkcji main. Funkcja ta, jak dobrze wiesz, wygląda następująco:

int main()
{

  // instrukcje
  return 0;
}

Funkcja ta jest pierwszą funkcją uruchamianą podczas działania programu. Tak będzie zawsze, nawet wówczas, gdy utworzymy kilka innych funkcji.

Patrząc na budowę funkcji main moglibyśmy stwierdzić, że ogólna budowa funkcji jest następująca:

typ nazwafunkcji()
{

  // instrukcje
  return wartoscTypuTyp;
}

I rzeczywiście, takie wnioski byłyby zupełnie poprawne, z tym tylko, że na szczęście funkcje potrafią nieco więcej. My po prostu z tych większych możliwości funkcji, w funkcji main nie korzystaliśmy.

W ogólnym przypadku, można powiedzieć, że schematyczna budowa funkcji przedstawia się następująco:

typ nazwafunkcji(ListaArgumentow)
{
  // instrukcje
  return wartoscTypuTyp;
}

Przy czym lista argumentów może być pusta (tak jak to było dotychczas w przypadku funkcji main) lub też jest postaci:

typ1 nazwaArgumentu, typ2 nazwaArgumentu2, ...

Z powyższego zapisu możesz się już domyślać, że do funkcji możemy przekazać kilka, kilkanaście, kilkadziesiąt, a nawet więcej argumentów. W praktyce jednak, liczba argumentów przekazywanych do funkcji rzadko kiedy jest większa od 10-20. Oczywiście jak już wspomniałem, funkcja może nie przyjmować żadnych argumentów.

Wróćmy jednak na moment do tego, co funkcja może zwracać. Funkcja, której używamy od samego początku kursu C++, czyli funkcja main() zwraca zmienną typu int. Skąd to wiadomo? - zapytasz. Jeśli przyjrzysz się naszej ogólnej budowie funkcji, zauważysz, że to jaki typ zwraca funkcja, określone jest przed nazwą funkcji. Ponieważ w naszych programach zapisywaliśmy int main(), to mam nadzieję, że oczywiste jest już dla Ciebie, że funkcja main() rzeczywiście zwraca typ int.

Co oznacza, że funkcja może zwracać jakąś wartość? Oznacza to tyle, że może ona zwrócić pewną wartość, która będzie mogła być wykorzystywana przez pozostałe części programu. Wynikiem takim może być np. wynik obliczeń lub wynik znalezienia/nieznalezienia jakiegoś znaku w ciągu znaków. W którym miejscu funkcja main zwraca wartość? Do zwracania rezultatów funkcji służy instrukcja return. Nasza funkcja main zwracała do tej pory zawsze wartość 0. Jak można łatwo zauważyć, zwracana wartość, czyli 0 jest typu int. Zatem wszystko się zgadza, bowiem przed nazwą funkcji stoi właśnie int, co jak już wiesz, oznacza jaki typ zwraca funkcja.

Pojawia się najprawdopodobniej u Ciebie teraz pewne zwątpienie - no dobrze, wszystko pięknie, ale do czego może nam służyć wartość zwracana przez funkcję? Rzeczywiście, w przypadku funkcji main, zwracana wartość nie jest nam bezpośrednio przydatna. Jednak już wkrótce dowiesz się, że w programie może być kilka funkcji i wówczas będziesz niewątpliwie wykorzystywać wartości zwracane przez pozostałe funkcje.

Funkcja main w rzeczywistości zwraca kod wykonania całego programu. Kod 0 oznacza, że program został wykonany poprawnie, natomiast zwrócenie innej wartości może oznaczać konkretny typ błędu. Dzięki temu programy mogą się ze sobą komunikować ? jeden program może uruchomić inny program i sprawdzić czy został on wykonany poprawnie czy też nie i wówczas podjąć jakieś dodatkowe działania.

No dobrze - rozumiesz już, że funkcja może zwracać wartość. Co jednak jeśli chcemy, żeby funkcja nie zwracała żadnej wartości? W przypadku argumentów funkcji można było przecież nie podawać żadnych argumentów. Czy można stworzyć funkcję, która nie zwraca żadnych wartości? Tak, okazuje się że w C++ jest to możliwe. Nie można jednak przed nazwą funkcji nie napisać żadnego typu, aby zasygnalizować, że funkcja nie zwraca żadnych wartości. Należy przed nazwą funkcji wpisać nazwę specjalnego typu void.

Jeśli zatem chcielibyśmy stworzyć funkcję, która nie zwraca nic i nie przyjmuje żadnych argumentów, powinna ona wyglądać tak:

void nazwaFunkcji()
{
return;
}

Co ciekawe, możemy również dla danej funkcji zaznaczyć, że nie przyjmuje ona żadnych argumentów, chociaż oczywiście już z powyższego przykładu widać, że argumentów nie ma. Używamy również do tego specjalnego typu void:

void nazwaFunkcji(void)
{
return;
}

Zwróć uwagę na instrukcję return znajdującą się w obu powyższych przykładach. Zauważ, że w obu przypadkach nie ma po niej żadnej wartości. Jak zatem widzisz, funkcja nie zwraca nic, co jest zupełnie zgodne z naszymi zamierzeniami. Wiedz, że jeśli funkcja nic nie zwraca, użycie instrukcji return nie jest konieczne. W przykładach jednak celowo instrukcję zostawiłem. Już wkrótce przekonasz się, że instrukcje return mogą służyć tak naprawdę nie tylko do samego zwracania wartości, ale też do przerywania działania funkcji.

Pierwsza przykładowa funkcja

Znając podstawowy schemat budowy funkcji, spróbujmy stworzyć prostą funkcję i wykorzystać ją w naszym programie. Zadaniem naszej funkcji niech będzie tylko i wyłącznie wypisanie komunikatu. Poniżej znajduje się przykładowy program:

#include <iostream>

using namespace std;

void wypisz()
{
  cout << "jestem funkcja wypisujaca"<<endl;
}


int main()
{
  cout <<"Zaraz \"uruchomie\" funkcje wypisz"<<endl;
  wypisz();
  cout <<"Funkcja zostala juz \"uruchomiona\""<<endl;
  getchar();
  return 0;
}
program nr 33.1

Jak łatwo można zauważyć, w programie oprócz funkcji main, pojawiła się funkcja wypisz. Przyglądając się argumentom funkcji oraz zwracanej wartości, można stwierdzić, że funkcja nie przyjmuje żadnych argumentów oraz nic nie zwraca. Jak widać jedynym zadaniem funkcji jest wypisanie komunikatu na standardowe wyjście.

Oprócz funkcji wypisz, w programie znajduje się znana Ci już dobrze funkcja main. Poza wypisaniem komunikatów na ekran, w funkcji tej jest wywołana funkcja wypisz. Jak widać wywołanie funkcji następuje poprzez napisanie jej nazwy oraz zawarcie w nawiasach okrągłych listy jej argumentów. W tym przypadku, jako że funkcja nie przyjmuje żadnych argumentów, lista argumentów podczas wywoływania funkcji jest pusta. Warto zwrócić jednak uwagę, że nawiasy po nazwie uruchamianej funkcji są niezbędne. Inaczej bowiem, kompilator nie będzie wiedzieć, że chcemy wywołać funkcję.

Tym co powinno zwrócić Twoją uwagę jest budowa programu. Jak dotąd zawsze na początku programu znajdowała się funkcja main. Tutaj jednak jest inaczej. Na początku programu znajduje się najpierw funkcja wypisz, a dopiero później znajduje się funkcja main. Czy tak musi być zawsze - zapytasz ? I tak i nie. Aby móc wywołać (czyli uruchomić) jakąś funkcję, musi być znana nazwa tej funkcji, argumenty tej funkcji oraz wartość zwracana. Jeśli zamienimy, kolejnością funkcje main i wypisz, to w funkcji main pojawia się wywołanie funkcji, która nie jest kompilatorowi znana. Kompilator w takiej sytuacji zgłosi błąd i kompilacja się nie powiedzie. Zgłoszony przez kompilator błąd będzie brzmiał podobnie jak 'wypisz' undeclared (first use this function).

Co jednak, jeśli w programie chciałbyś stworzyć kilka własnych funkcji? Czy należy dbać o kolejność ich zapisania w programie w C++? A co jeśli funkcje wywołują siebie nawzajem? Rzeczywiście, gdybyśmy używali dotychczasowej metody, zrealizowanie pewnych bardziej skomplikowanych przypadków byłoby niemożliwe. Na przykład, gdyby funkcja A wywoływała w pewnych przypadkach funkcję B, a funkcja B wywoływałaby w pewnych okolicznościach funkcję A, to niemożliwe byłoby ustalenie, która funkcja powinna pojawić się w programie jako pierwsza - niezależnie, która funkcja pojawiłaby się w programie jako pierwsza, kompilator zgłosiłby błąd, bo nie byłaby znana druga funkcja. Na szczęście problem tego typu da się stosunkowo łatwo rozwiązać.

Definicja a deklaracja funkcji

Aby rozwiązać problem kolejności zapisu funkcji w programie, posłużymy się pojęciami definicja i deklaracja. Od razu zaznaczam, że jeśli nie pamiętasz co oznaczają te pojęcia, zapraszam do zapoznania się z lekcją o tytule Definicja, deklaracja, inicjalizacja - ważne pojęcia w języku C++.

Jak już wcześniej wspomniałem, aby móc wywołać daną funkcję, w programie wcześniej musi pojawić się jej nazwa, lista argumentów oraz wartość zwracana. Do tej pory, w przykładowym programie umieściliśmy definicję funkcji wypisz - pojawiła się tu bowiem nazwa funkcji wraz z listą argumentów oraz wartością zwracaną, ale jednocześnie opis działania funkcji. Okazuje się jednak, że aby móc w danej funkcji umieścić wywołanie innej funkcji, wystarczy umieścić wcześniej zaledwie deklarację uruchamianej funkcji.

Aby udało Ci się dokładniej zrozumieć, o czym piszę:

void wypisz()
{
  cout << "jestem funkcja wypisujaca"<<endl;
}

jest definicją funkcji wypisz, natomiast:

void wypisz();

jest deklaracją funkcji. Zwróć uwagę, że w deklaracji funkcji, po liście argumentów i nawiasie znajduje się średnik.

Oczywistą sprawą jest jednak, że sama deklaracja funkcji w programie nie wystarczy - potrzebna jest również jej definicja. Gdybyśmy w programie nie umieścili definicji funkcji, to nie dość, że kompilator zgłosi błąd, to na dodatek przecież kompilator nie czyta w naszych myślach i nie wie, co dana funkcja miałaby w programie wykonywać. Dlatego też, w programie należy w takim przypadku umieścić zarówno definicję, jak i deklarację funkcji. Deklaracja powinna być umieszczona na początku programu, a definicja może zostać umieszczona w dowolnym miejscu.

W przypadku stosowania funkcji w programie przyjęło się jednak, że na początku programu są umieszczane deklaracje funkcji, następnie jest umieszczana definicja funkcji main, a dopiero po funkcji main są umieszczane definicje pozostałych funkcji. Dzięki takiemu rozmieszczeniu definicji funkcji w programie, można stosunkowo łatwo zorientować się, co robi program, bowiem definicja funkcji main jest stosunkowo blisko początku programu.

Oto przykładowy kod, w którym znajduje się zarówno deklaracja jak i definicja funkcji wypisz:

#include <iostream>

using namespace std;

void wypisz(); // to jest deklaracja funkcji wypisz

int main()
{
  cout <<"Zaraz \"uruchomie\" funkcje wypisz"<<endl;
  wypisz();
  cout <<"Funkcja zostala juz \"uruchomiona\""<<endl;
  getchar();
  return 0;
}

void wypisz() // to jest definicja funkcji wypisz
{
  cout << "jestem funkcja wypisujaca"<<endl;
}
program nr 33.2

Rozdzielenie deklaracji i definicji funkcji zagwarantuje nam, że nie napotkamy nigdy na problem, która z funkcji powinna pojawić się w programie jako pierwsza. Dlatego też dobrze radzę - zapamiętaj tę regułę i stosuj ją zawsze w swoich programach.

Funkcje przyjmujące argumenty i zwracające wartość

Jak na razie przedstawiłem Ci funkcję, której celem było wypisanie informacji na standardowe wyjście. Oczywiście takie funkcje również mają prawo bytu w programach, jednak zazwyczaj funkcje są wykorzystywane do bardziej skomplikowanych czynności. Czynnościami takimi są na przykład obliczenia. Funkcje wykonujące obliczenia bardzo często przyjmują pewne argumenty do wykonania obliczeń, jak również zwracają wynik swoich obliczeń.

Naszym zadaniem będzie napisanie funkcji, która będzie miała obliczyć wartość pewnej liczby podniesionej do potęgi naturalnej (czyli 0,1,2 itd.). Oczywiście w C++ tego typu funkcja już istnieje (jest to funkcja pow z biblioteki cmath), jednak dla treningu stworzymy sami taką właśnie funkcję.

Pierwsza sprawa to nazwa funkcji. Jako, że funkcja ma służyć do potęgowania, nazwiemy naszą funkcję potega. Czas zastanowić się, czy funkcja będzie przyjmowała jakieś argumenty oraz czy będzie zwracała wartość. Po chwili przemyślenia dojdziemy do wniosku, że tak, bowiem chcemy umożliwić podnoszenie dowolnej liczby do dowolnej naturalnej potęgi i musimy danej funkcji przekazać w jakiś sposób zarówno liczbę, którą chcemy potęgować, jak i wartość potęgi. Funkcja będzie zatem przyjmowała 2 argumenty. Jako, że liczba potęgowana może być dowolnego typu, ustalamy typ tej liczby jako double. Natomiast typ potęgi, do której będzie podnoszona liczba ustalimy jako unsigned int, czyli liczba naturalna. Ostatnią kwestią jest wartość zwracana przez funkcję. Czy funkcja powinna zwracać jakąś wartość? Oczywiście, że tak - jako, że funkcja dokonuje pewnych obliczeń musi w jakiś sposób przekazać wartość swoich obliczeń. Jako, że funkcja będzie podnosiła do potęgi liczbę typu double (tak przed momentem ustaliliśmy), to w tym przypadku zwracany wynik powinien być również typu double.

Przykładowy program, w którym znajduje się definicji funkcji potega mógłby wyglądać następująco:

#include <iostream>

using namespace std;

double potega(double liczba, unsigned int potega);

int main()
{
  double a=2.0, w;
  w = potega(a,3); // 1
  cout <<a<<" do potegi 3 to "<<w<<endl;
 
  cout <<a<<" do potegi 4 to "<<potega (a,4)<<endl; // 2
 
  cout <<"3 do potegi 2 to "<<potega(3,2)<<endl; // 3
  getchar();
  return 0;
}

double potega(double liczba, unsigned int potega)
{
  double wynik=1;
 
  for (unsigned int i=1;i<=potega;++i)
     wynik*=liczba;
 
  return wynik;
}
program nr 33.3

Jak widać, zarówno definicja, jak i deklaracja funkcji są identyczne - tak oczywiście powinno być. Nie możemy najpierw powiedzieć kompilatorowi, że funkcja przyjmuje 3 argumenty, a później twierdzimy, że przyjmuje tylko 2 argumenty. Zatem w deklaracji i definicji funkcji zarówno liczba argumentów, ich typ, jak i typ zwracany przez funkcję są identyczne.

Sama funkcja potega tworzy zmienną wynik, która ma służyć do obliczenia potęgi danej liczby. Jak widać, obliczenie potęgi odbywa się poprzez pomnożenie liczby odpowiednią ilość razy. Następnie funkcja zwraca wartość zmiennej wynik.

Jeśli przyjrzymy się teraz funkcji main, można zauważyć, jak różnie można wykorzystać funkcję potega. W miejscu opatrzonym komentarzem // 1 do funkcji przekazujemy jako pierwszy argument zmienną a, natomiast jako drugi argument literał typu int. Widać zatem, że jako argumenty możemy przekazywać zarówno zmienne, jak i literały. W tym samym miejscu, wynik funkcji przypisujemy do zmiennej typu double, a następnie dopiero wypisujemy otrzymany rezultat.

Z kolei w miejscu opatrzonym komentarzem // 2 wartości zwróconej przez funkcję nie przypisujemy do żadnej zmiennej, a po prostu wypisujemy na standardowe wyjście. Okazuje się, że wartość zwracaną przez funkcję można wykorzystywać jak zwykły literał (w tym przypadku jest to literał typu double) i można dokonywać na nim wszystkie operacje dozwolone dla danego typu (np. dodawanie). Jest to oczywiście możliwe tylko w sytuacji, kiedy funkcja zwraca wartość - w przeciwnym przypadku wyniku zwracanego przez funkcję nie będziemy oczywiście mogli wykorzystać.

W miejscu opatrzonym komentarzem // 3 pokazane jest, że również oba argumenty mogą być przekazane jako literały, co jak widać potwierdza to tezę, że do funkcji można przekazywać zarówno zmienne, jak i literały, a wynik uruchomienia funkcji pozostanie taki sam.

Funkcje - dodatkowe informacje

W tym momencie posiadasz już podstawową wiedzę dotyczącą funkcji w języku C++. Warto jednak już teraz rozszerzyć nieco te informacje.

Pierwszą ciekawostką dotyczącą funkcji jest to, że w przypadku, kiedy funkcja ma oddzielną deklarację i definicję (tak będzie już teraz we wszystkich naszych programach, o czym dobrze już wiesz), nie jest konieczne nazywanie argumentów funkcji podczas deklaracji. Wystarczą jedynie typy tych argumentów. Często używa się tej właściwości, żeby nie zaciemniać deklaracji funkcji niepotrzebnymi nazwami argumentów.

void test(int, double);

Powyższa deklaracja funkcji to deklaracja funkcji test, przyjmującej dwa argumenty - pierwszy typu int, drugi typu double. Jak widać nie ma tutaj nigdzie nazw tych argumentów - pojawiłyby się one w deklaracji funkcji.

Oprócz tego, warto dodatkowo wiedzieć, że instrukcja return może pojawić się w dowolnym miejscu funkcji i przerywa ona wówczas działanie funkcji. Jeśli funkcja zwraca jakąś wartość, oczywiście po słowie return powinna pojawić się wartość, natomiast jeśli funkcja nic nie zwraca (zwraca typ void), to po słowie return nic nie umieszczamy (poza średnikiem).

Oba powyższe zagadnienia niech zilustruje program, w którym sprawdzamy, czy liczba jest liczbą pierwszą czy nie. Sprawdzanie, czy liczba jest liczbą pierwszą odbywa się poprzez sprawdzanie czy dzieli się ona bez reszty przez któreś z liczb od 2 do pierwiastka ze sprawdzanej liczby.

#include <iostream>
#include <cmath>

using namespace std;


// sprawdza czy liczba jest liczba pierwsza
bool czyPierwsza(unsigned long int);

// wypisuje komunikat czy liczba jest liczba pierwsza
void wypisz(unsigned long int, bool);

int main()
{
  unsigned long int liczba;
  bool wynik;
 
  cout <<"Podaj liczbe ktora chcesz sprawdzic: ";
  cin >>liczba;
  cin.ignore();
 
  wynik = czyPierwsza(liczba);
  wypisz(liczba,wynik);
 
  cout <<endl<<"Nacisnij ENTER aby zakonczyc..."<<endl;
  getchar();  
  return 0;  
}


bool czyPierwsza(unsigned long int licz)
{
  for (unsigned int i=2;i<=sqrt((long double) (licz));++i)
     if (!(licz % i))
        return false;
  return true;
}

void wypisz(unsigned long int licz, bool pierwsza)
{
  cout <<"Sprawdzona liczba to liczba "<<licz<<endl;
  if (pierwsza)
  {
     cout <<"Liczba ta jest liczba pierwsza"<<endl;
     return;
  }
  cout <<"Liczba ta NIE jest liczba pierwsza"<<endl;
}
program nr 33.4

Jak widać, w przykładowym programie, mamy zadeklarowane 2 funkcje - obie funkcje w deklaracji nie mają nazw argumentów. Nazwy argumentów pojawiają się za to oczywiście w definicjach funkcji.

Dodatkowo, w obu funkcjach jest wykorzystana instrukcja return. W funkcji czyPierwsza wykorzystujemy instrukcję return do zwrócenia rezultatu, czy liczba jest liczbą pierwszą czy nie. Warto jednak również zwrócić uwagę, że sama instrukcja return służy także do przerwania działania funkcji. Jeśli liczba nie jest liczbą pierwszą, to wykona się instrukcja return false; , a instrukcja return true; nie wykona się już nigdy.

Podobnie jest z funkcją wypisz. Tutaj jednak instrukcja return służy tylko i wyłącznie do przerwania działania funkcji. Zauważ, że gdyby instrukcji return nie było, zostałyby wypisane oba komunikaty, co nie byłoby prawidłowe. Oczywiście w tym przypadku można by było wykorzystać również instrukcję if - else zamiast instrukcji return, jednak jak widać, niekiedy instrukcja return w funkcjach, które nawet nie zwracają żadnej wartości może być przydatna.

Podstawy funkcji - podsumowanie

W tej lekcji przedstawiłem Ci podstawowe informacje dotyczące funkcji. Wiesz już jak deklarować i definiować funkcje, w jaki sposób przekazywać do nich parametry, jak zwracać wyniki działania funkcji i umiesz wywoływać funkcje.

To jednak nie wszystko - funkcje w C++ mają o wiele więcej możliwości i musisz dowiedzieć się o nich znacznie więcej, aby się nimi sprawnie posługiwać. Zapraszam zatem do pogłębienia wiedzy w kolejnych lekcjach kursu.

powrót