. FUNKCJE
Jeśli tworzenie programu w C++ porównamy do budowania domku z klocków, to funkcje są właśnie takimi klockami. Są to podprogramy działające pod kontrolą głównej funkcji programu - main(). Rozwiązanie takie sprzyja nie tylko pracy grup programistów (pracę nad programem można podzielić na części), ale też znajdowaniu potencjalnych błędów - łatwiej przecież znaleźć pomyłkę w mniejszych fragmentach kodu.
Deklaracja funkcji wygląda następująco: typ_zwracany nazwa_funkcji(argumenty).
Gdy użyjemy funkcji w programie, może ona w swoje miejsce zwrócić wartość określonego typu. Jeśli będzie to np. typ int (przypomnij sobie wiadomości z 1 części kursu na temat typów danych), to wartość zwracana tejże funkcji będzie traktowana w programie jako najzwyklejsza w świecie liczba całkowita (wartość zwracaną podajemy po słowie return). Argumentami funkcji będą zaś wartości, które wprowadzamy do funkcji i na których (najczęściej) funkcja ta pracuje.
Przykładowo, jeśli stworzymy funkcję sumującą dwie liczby a i b, to argumentami będą a i b, a wartością zwracaną ich suma. W praktyce definicja tej funkcji wygląda:
int dodaj(int a, int b) /* funkcja zwraca liczbe calkowita, a pobiera dwie takie liczby: a oraz b */
{
int suma;
suma = a + b;
return suma; // funkcja zwraca wartosc zmiennej suma
}
Do funkcji w programie odnosimy się bezpośrednio przez podanie jej nazwy i argumentów, czyli np. dodaj(3, 4). Jednak zanim z niej skorzystamy, powinniśmy "zgłosić" ją kompilatorowi, gdyż taka instrukcja: dodaj(liczba1, liczba2) jest dla niego (bez deklaracji) kompletnie niezrozumiała.
Istnieją dwa sposoby przekazania kompilatorowi informacji o funkcji: deklaracja przed, a definicja (tzn. cała treść funkcji) po funkcji main() lub też cała definicja przed main(). Przeanalizujmy rzecz na konkretnym przykładzie:
#include <iostream.h>
int odejmowanie(int a, int b); // deklaracja (nie definicja!) funkcji odejmowanie
int dodawanie(int a, int b) // poczatek definicji funkcji dodawanie
{
return a + b; // funkcja zwraca sume a i b
} // koniec definicji funkcji dodawanie
main ()
{ // poczatek programu
int suma, roznica;
suma = dodawanie(25, 28); /* do zmiennej suma wpisujemy wartosc zwracana przez funkcje */
roznica = odejmowanie(25, 28);
} // koniec programu
int odejmowanie(int a, int b) // poczatek definicji
{
return a - b; // funkcja zwraca roznice a i b
}
Zapewne domyślasz się już, czym różni się deklaracja od definicji, ale dla porządku możemy zaznaczyć: deklaracja jedynie sygnalizuje kompilatorowi, że coś się pojawi, podczas gdy definicja konkretnie określa co (tak to wygląda na "chłopski rozum"). Należy pamiętać, że każda definicja jest jednocześnie deklaracją. Tylko i wyłącznie od nas zależy, który sposób określania funkcji w programie będziemy stosować.
Czasami jednak nie chcemy, by funkcja zwracała jakąkolwiek wartość - po prostu mają się wykonać zawarte w niej instrukcje i tyle. Aby funkcja nie zwracała żadnych wartości określamy jej typ jako void. Przykladowo:
void suma (int a, int b) // typ zwracany jako void
{
cout << "Suma: " << a + b;
} // nie uzywamy return, bo nie ma co zwracac!
main()
{
suma(4, 5); // funkcja wyswietla sume 2 liczb
}
Jeśli nie chcemy natomiast pobierać żadnych argumentów, to teoretycznie też powinniśmy wpisać void w ich miejsce. Jednak w praktyce wystarczy, że pozostawimy pusty nawias, np. int funkcja().
Przykład funkcji bezargumentowych:
#include <iostream.h>
void pisz1();
void pisz2();
main()
{
int a;
cin >> a;
if (a == 1) pisz1();
else pisz2();
}
void pisz1()
{
cout << "Wybrales liczbe 1";
}
void pisz2()
{
cout << "Nie wybrales 1";
}
Funkcje mogą pracować nie tylko na argumentach - możemy również zdefiniować w ich wnętrzu zmienne (w taki sam sposób, jak zwykle). Przykład:
int funkcja(int a, char b)
{
int c = 28; // definicja zmiennej wewnatrz funkcji
return a + b + c;
}
Pomyślmy: zmienne możemy definiować poza funkcją oraz w jej wnętrzu. Co się więc stanie, jeśli zdefiniujemy zmienną o nazwie liczba poza funkcją, a potem drugą taką samą, ale już w środku funkcji? Jak się okazuje, że nie będzie to błąd - jako liczba traktowana będzie zmienna zdefiniowana "bardziej prywatnie", a więc wewnątrz funkcji. Wyróżniamy bowiem zmienne o różnym zakresie ważności:
- lokalne (dostępne tylko wewnątrz funkcji)
- globalne (dostępne w całym programie)
Podział ten w rzeczywistości jest nieco bardziej rozgałęziony, ale zatrzymajmy się na razie na przedstawionym.
Zobaczmy przykład:
#include <iostream.h>
int liczba = 28; // definicja zmiennej globalnej
void funkcja();
main()
{
cout << "Liczba = " << liczba << endl;
funkcja();
cout << "Liczba = " << liczba;
}
void funkcja()
{
int liczba = 25; // definicja zmiennej lokalnej
cout << "Liczba = " << liczba << endl;
}
Po wykonaniu programu na ekranie zobaczymy coś takiego:
Liczba = 28
Liczba = 25
Liczba = 28
Jak więc widzimy, zmienna lokalna zasłoniła zmienną globalną i to do niej została wpisana wartość 25.
Dygresja na temat zmiennych: zmienne możemy definiować naprawdę w dowolnym miejscu w programie (byle tylko przed pierwszym zastosowaniem w programie - nie możemy korzystać z czegoś, co nie zostało jeszcze stworzone). Jednak aby zmienna była widziana we wszystkich funkcjach, należy ją zdefiniować jeszcze przed rozpoczęciem funkcji main().
Wróćmy jeszcze na chwilę do argumentów funkcji. Przeanalizujmy przykład:
#include <iostream.h>
void funkcja(int liczba)
{
liczba = 28;
cout << liczba;
}
main()
{
int liczba = 25;
funkcja(liczba);
cout << liczba;
}
Mimo że funkcja zadziałała, na ekranie nie pojawiła się 2 razy liczba 28, ale 28 i 25. Jaki stąd wniosek? Otóż każda funkcja tworzy kopie argumentów - jeżeli odwołujemy się do jakiegoś argumentu, to w rzeczywistości odnosimy się do jego kopii - "pierwotna" zmienna pozostaje bez zmian.
Możemy zmieniać jednak pobierane argumenty. Istnieje na to kilka różnych metod, my skorzystamy z referencji. Do funkcji będzie przekazywany jedynie adres zmiennej w pamięci, a nie jej wartość, dzięki czemu wszystkie zmiany będą dokonywane bezpośrednio na zmiennej "macierzystej". Aby skorzystać z referencji, należy użyć znaku &, np. char &zmienna.
Przykładowo:
#include <iostream.h>
void funkcja(int &zmienna1, int zmienna2) // pobranie zmiennej przez referencje
{
zmienna2 = 25;
zmienna1 = 25; // przypisanie 25 do zmiennej GLOBALNEJ
}
main()
{
int liczba1 = 28, liczba2 = 28;
cout << "Liczba1 przed zmiana: " << liczba1 << endl;
cout << "Liczba2 przed zmiana: " << liczba2 << endl;
funkcja(liczba1, liczba2);
cout << "Liczba1 po zmianie: " << liczba1 << endl;
cout << "Liczba2 po zmianie: " << liczba2;
}
Napiszmy program podający drogę w ruchu jednostajnie prostoliniowym (S=vt) lub jednostajnie zmiennym (S=1/2 at2 + v0t):
#include <iostream.h>
float ruch_zmienny()
{
float czas, przyspieszenie, predkosc_p;
cout << "Podaj czas (s): ";
cin >> czas;
cout << "Podaj przyspieszenie (m/s*s): ";
cin >> przyspieszenie;
cout << "Podaj predkosc poczatkowa (m/s): ";
cin >> predkosc_p;
return (przyspieszenie*czas*czas)/2 + predkosc_p*czas;
}
float ruch_prostoliniowy()
{
float predkosc, czas;
cout << "Podaj predkosc (m/s): ";
cin >> predkosc;
cout << "Podaj czas (s): ";
cin >> czas;
return predkosc*czas;
}
main()
{
int decyzja;
char ponownie;
do
{
cout << endl << "Droga w ruchu prostoliniowym - 0, w ruchu zmiennym - 1." << endl << "Wybierz: ";
cin >> decyzja;
if (decyzja)
cout << << "Droga wynosi: " << ruch_zmienny() << endl << endl;
else cout << endl << "Droga wynosi: " << ruch_prostoliniowy() << endl << endl;
cout << "Jeszcze raz (t/n)? ";
cin >> ponownie;
} while (ponownie != 'n');
}
W C++ istnieje coś takiego, jak argumenty domniemane. Zdarza się czasem, że wywołujemy daną funkcję i jeden (albo i więcej) argument prawie zawsze jest taki sam. Można mu więc przypisać domyślną wartość, aby przy odwoływaniu się do funkcji nie trzeba było podawać wszystkich wartości. Możemy na przykład napisać:
int suma(int a = 2, int b)
{
return a + b;
}
Funkcję taką możemy wywołać w dwojaki sposób - podając 1 lub 2 argumenty. Jeśli podamy tylko jeden - drugi będzie domyślny. Przykłady wywołania:
suma(25); // a jest domniemane i wynosi 2
suma(28, 25); // a wynosi 28
Wyobraźmy sobie teraz, że piszemy program wspólnie z kolegą. Dzielicie się pracą, każdy pisze własne funkcje. Jak je jednak połączyć? Można oczywiście skopiować i wkleić kod, ale po co? Istnieje przecież możliwość dołączania funkcji w osobnych plikach. Służy do tego nic innego, jak dobrze już nam znana dyrektywa #include. Piszemy: #include "nazwa_pliku", wskazując na plik, w którym zawarta jest definicja funkcji.
Przykład:
Plik definicja.h:
int suma(int a, b)
{
return a + b;
}
Plik programu - suma.cpp:
#include <iostream.h>
#include "definicja.h"
main()
{
cout << suma(25, 28);
}
Zwróćmy na koniec uwagę na estetykę dołączania plików. Ogólnie przyjętym standardem jest wykorzystywanie cudzysłowia " " przy dołączaniu własnych plików i nawiasów < > przy dołączaniu standardowych plików dostarczonych wraz z kompilatorem. Starajmy się nie odbiegać zbyt często od tych norm.
Do zapamiętania z rozdziału:
- funkcje pozwalają podzielić program na mniejsze części
- funkcje mogą zwracać wartość i pobierać argumenty
- funkcję w programie wywołujemy przez podanie jej nazwy i argumentów
- typ void oznacza brak zwracanej wartości
- aby odnieść się w funkcji bezpośrednio do zmiennej (a nie do jej wartości) możemy zastosować referencję (np. &zmienna = 25)
- argumenty domniemane pozwalają na uruchamianie funkcji bez podawania wszystkich parametrów
- definicje funkcji możemy dołączać w plikach nagłówkowych przy pomocy dyrektywy #include
Zadanie:
Zmodyfikuj program do obliczania drogi w ruchach jednostajnych, który napisaliśmy na tej lekcji. Program ma mieć możliwość pobierania danych w różnych jednostkach (np. km/h, min).
W następnej części kursu powiemy sobie o przeładowaniach nazw funkcji, nowych typach danych oraz