Funkcje
Kiedy ktoś mówi o C++ to ma na myśli przede wszystkim obiekty. Jednak
obiekty bazują, na funkcjach, które wykonują niezbędne do działania obiektu
operacje.
Każdy program w C++ posiada przynajmniej jedną funkcję, main () . Kiedy
uruchamia się program to funkcja main () jest automatycznie uruchamiana.
Może ona wywoływać inne funkcje, które z kolei mogą wywołać jeszcze inne.
Funkcja jest podprogramem, który może modyfikować dane i zwracać
wartość.
Każda funkcja ma swoją nazwę, a wywołanie funkcji polega na wpisaniu jej
nazwy w programie. W momencie napotkania wywołania funkcji program
przechodzi do wykonania kodu funkcji. Kiedy funkcja się kończy, to program
wraca do miejsca jej wywołania (do następnej instrukcji).
Ilustracja tego procesu
return;
Funkcja
4
Main()
{
Instrukcja;
Funkcja1 () ;
Instrukcja ;
Funkcja2 () ;
Instrukcja ;
Funkcja4 () ;
}
return;
Funkcja
1
return;
Funkcja
3
Instrukcja;
Funkcja3();
return;
Funkcja
2
Deklarowanie funkcji (prototypy)
Deklaracja funkcji określa jej nazwę, typ zwracanej wartości i listę
parametrów.
Standardowe funkcje dostarczone razem z kompilatorem posiadają już swoje
prototypy. Wystarczy dołączyć za pomocą #include odpowiedni plik
nagłówkowy (.H)
Prototyp funkcji to typ wartości zwracanej przez tę funkcję, nazwa i lista
parametrów. Prototyp kończy się znakiem średnika.
Lista
parametrów
to
wyszczególnienie
wszystkich
parametrów
przekazywanych do funkcji oddzielonych przecinkami.
unsigned short int PoliczPole ( int nDlugosc, int nSzerokosc);
typ zwracanej
wartości
nazw
a
parame
try
średn
ik
nazwa
parametru
typ
parametru
Przykład: „Elementy prototypu”
Żadna funkcja nie może być wywołana przez inną bez uprzedniego
zadeklarowania.
Deklaracja funkcji nazywana jest prototypem.
Definicja obejmuje treść funkcji.
Prototyp, pod względem typu wartości zwracanej, nazwy i typów parametrów,
musi zgadzać się z definicją funkcji.
Jeśli wystąpią różnice, to kompilator, przy próbie kompilacji,
zasygnalizuje błąd.
Przykład: long
Pole
( int , int );
Ten prototyp deklaruje funkcję
Pole ()
zwracającą wartość typu long i
posiadającą dwa parametry typu int. Mimo że taka deklaracja jest całkowicie
prawidłowa, to dla poprawienia przejrzystości zalecane jest podawanie w
prototypie również nazw parametrów. Deklaracja z nazwami parametrów
będzie wyglądać następująco:
long
Pole
( int nDlugosc, int nSzerokosc
) ;
Zauważmy, że wszystkie funkcje mają określony typ zwracanej wartości.
Prototyp funkcji mówi kompilatorowi o nazwie funkcji, wartości zwracanej i
parametrach.
typ_zwracany
nazwa_funkcji
( [
typ
[
nazwa_parametru
]]….)
;
składnia:
Porównajcie prototyp z definicją
funkcji.
Zwróćcie uwagę, że typ wartości
zwracanej,
nazwa
i
typy
parametrów są identyczne.
Gdyby były jakiekolwiek różnice to
kompilator wygenerowałby błąd.
Praktycznie jedyna wymagana
różnica polega na tym, ze
prototyp kończy się średnikiem
i nie zawiera treści funkcji.
Zwróćcie także uwagę na to, że
nazwy parametrów w prototypie to
nDlugosc i nSzerokosc, a nazwa
parametrów w definicji to d i s. Jak
widać,
nazwy
parametrów
w
prototypie nie są używane, służą
one jedynie jako informacja dla
programisty.
Definiowanie funkcji
Definicja funkcji składa się z nagłówka i treści funkcji. Nagłówek wygląda tak,
jak prototyp, jednak musi posiadać nazwy parametrów i nie może być
zakończony średnikiem.
Treści funkcji to zbiór instrukcji ograniczony klamrami.
unsigned short int ZnajdzPole ( int nDługosc, int
nSzerokosc )
{
// instrukcje
return ( nDlugosc*nSzerokosc ) ;
}
Klamra
zamykająca
Klamra
otwierająca
słowo
kluczowe
zwracana
wartość
typ zwracanej
wartości
nazw
a
parametry
Definicja mówi kompilatorowi co dana funkcja robi i jak działa.
typ_zwracany
nazwa_funkcji
( [
typ
[
nazwa_parametru
]]…. )
{
instrukcje;
}
Składnia:
Jeżeli funkcja zwraca jakąś wartość, to przed wyjściem z funkcji należy użyć
instrukcji return. Instrukcja ta może zostać użyta w każdym miejscu treści
funkcji.
Dla każdej funkcji określany jest typ wartości zwracanej. Jeśli nie podamy
typu, to automatycznie zostanie przypisany typ całkowity – int. Jeśli funkcja
nie zwraca żadnej wartości, to typem zwracanym będzie void.
Przykłady prototypów funkcji:
long Pole (long lDlugosc, long lSzerokosc);
Zwraca long,
ma dwa parametry
void WypiszTekst(int nNumerTekstu);
Zwraca void,
ma jeden parametr
int PobierzOpcje ();
Zwraca int,
brak parametrów
SpecFunk () ;
Zwraca int,
brak parametrów
Przykłady poprawnych definicji funkcji:
void WypiszTekst( int nNumerTekstu )
{
if ( nNumerTekstu == 0)
{
cout << "Czesc.\n";
}
if ( nNumerTekstu == 1)
{
cout << "Do widzenia. \n";
}
if ( nNumerTekstu > 1)
{
cout << "Jestem troche zaklopotany. \n";
}
}
long Pole ( long d, long s )
{
return d*s;
}
Zmienne lokalne
Zmienne można nie tylko przekazywać do funkcji. Można je również
deklarować wewnątrz funkcji. Wykorzystuje się w tym celu tzw. zmienne
lokalne. Zmienne te są widoczne tylko wewnątrz funkcji, w której są
zadeklarowane. Kiedy funkcja się kończy, zmienne przestają być dostępne.
Zmienne lokalne definiuje się tak samo jak wszystkie inne. Parametry
przekazywane do funkcji również są traktowane jako zmienne lokalne i można
je wykorzystywać tak, jakby były wewnątrz tej funkcji zadeklarowane.
Wartość
przekazywana
jako
parametr, fTempFer, również
jest tylko lokalną kopią zmiennej
przekazywaną z funkcji main ().
Deklarowana
jest
zmienna
lokalna fTempCel. Ta zmienna
istnieje tylko wewnątrz funkcji
Konwertuj ().
Zwróćmy uwagę, że nie jest to ta
sama zmienna co fTempCel.
Każda zmienna ma swój zasięg,
który mówi jak długo zmienna
jest dostępna i gdzie można z niej
korzystać.
Zmienne
zadeklarowane w osobnym bloku
widoczne są tylko wewnątrz niego
i giną wraz z końcem bloku.
Zmienne globalne
Zmienne zadeklarowane poza wszystkimi funkcjami mają globalny zasięg i
widoczne są wewnątrz wszystkich funkcji, łącznie z main (). Gmatwają one
bardzo program.
W profesjonalnie napisanych programach, są bardzo rzadko
spotkane.
Argumenty funkcji
Argumenty funkcji nie muszą być tego samego typu. Nie ma żadnych
przeciwwskazań, żeby argumentami funkcji były np.: jedna zmienna typu int,
dwie typu float i jedna typu char.
Każde poprawne wyrażenie może być argumentem funkcji. Mam tu na myśli
również stałe, wyrażenia matematyczne i logiczne oraz inne funkcje
zwracające wartość.
Funkcje jako parametry innych funkcji
Mimo że można używać funkcji zwracających wartości jako parametry do
innych funkcji, to prowadzi ono do zbędnego komplikowania kodu.
Przykład:
Załóżmy, że mamy funkcje RazyDwa (), RazyTrzy (), Kwadrat () i Szescian () ,
z których każda zwraca wartość. Można napisać:
lOdp = ( RazyDwa( RazyTrzy( Kwadrat( Szescian( lWartosc )))));
Ta instrukcja pobiera zmienną lWartosc, przekazuje ją jako argument do
funkcji Szescian (), zwracana wartość przekazywana jest z kolei od funkcji
Kwadrat(), której wynik przekazywany jest do funkcji RazyTrzy () w
rezultacie której operuje funkcja RazyDwa () . Wynik tego podwajania,
potrajania i potęgowania podstawiany jest do zmiennej lOdp.
Trudno na pierwszy rzut okna powiedzieć co ta instrukcja robi (czy wartość
była podnoszona do sześcianu zanim była podwojona czy nie?).
Również w momencie uzyskania wyniku niezgodnego z oczekiwaniami trudno
będzie znaleźć miejsce popełnienia błędu.
Alternatywnym rozwiązaniem jest przypisanie każdego kroku do osobnych
zmiennych:
unsigned long lWartosc = 2 ;
unsigned long lPotega3 = Szescian( lWartosc );// lPotenga3 =
8
unsigned long lPotega2 = Kwadrat( lPotega3 ); // lPotenga2 =
64
unsigned long lPrzez3 = RazyTrzy( lPotega2 );// lPrzez3 =
192
unsigned long lPrzez2 = RazyDwa( lPrzez3 ); // lPrzez2 =
384
Teraz każda pośrednia wartość może zostać przeanalizowana. Jasna jest
również kolejność wykonywania operacji.
Parametry są zmiennymi lokalnymi
Argumenty przekazywane do funkcji są w niej lokalne. Zmiana ich wartości
jest również lokalna i nie jest widoczna w funkcji wywołującej. Nazywamy to
przekazywaniem przez wartość, co oznacza, że w funkcji tworzona jest
lokalna kopia każdego argumentu przekazywanego do tej funkcji. Takie kopie
są traktowane tak, jak lokalne zmienne.
Ten program, w funkcji main() ,
inicjalizuje
dwie
zmienne
i
przekazuje je do funkcji Zamien(),
która teoretycznie je zamienia.
Jak
widać,
zmienne
zostały
przekazane do funkcji tylko przez
wartość. Zostały stworzone ich
lokalne kopie, i na tych kopiach
były
wykonywane
wszelkie
operacje. Były to lokalne zmienne
funkcji Zamien(). Zamiana została
dokonana tylko na kopiach i nie
miała żadnego wpływu na wartości
zmiennych w funkcji main().
Jednak po przetestowaniu wyniku w
funkcji main() okazuje się, że nic
się nie zmieniło!
Zwracanie wartości
Funkcje mogą zwracać albo jakąś wartość albo void. void to informacja dla
kompilatora, że funkcja nie będzie zwracać wartości. Żeby zwrócić wartość z
funkcji, należy użyć słowa kluczowego return, a następnie podać wartość,
która ma zostać zwrócona. Równie dobrze może być to wyrażenie zwracające
wartość.
Przykład:
return 5 ;
return ( x > 5 ) ;
return (MojaFunkcja() );
Po napotkaniu słowa kluczowego return wartość wymieniona po return jest
zwracana jako wartość funkcji, a program wraca do funkcji wywołującej.
Krótko mówiąc, instrukcja return, kończy wykonywanie danej funkcji.
Wartość zwrócona będzie równa zero
jeżeli x będzie nie większe niż 5, w
przeciwnym wypadku będzie równa
1. To co jest zwracane to wartość
wyrażenia, 0 (fałsz) lub 1 (prawda), a
nie wartość zmiennej x.
W jednej funkcji można wielokrotnie wykorzystywać instrukcję return.
Wykonanie instrukcji return powoduje zakończenie funkcji.
Funkcja Denominator() sprawdza, czy podana
liczba
nie
jest
większa
od
stałej
GRANICAGORNA
. Jeżeli nie, to funkcja zwraca
jej
wartość
pomnożoną
przez
stałą
DEWALUACJA
. Jeżeli jednak liczba jest większa
od 1000, to funkcja zwraca stałą
ERROR
jako
wartość błędną.
Instrukcja nigdy nie zostanie wykonana,
ponieważ niezależnie od wartości parametru
(większy od 1000 czy nie) funkcja zawsze wróci
albo albo .
Dobry kompilator zasygnalizuje, że ta
instrukcja
nigdy nie zostanie osiągnięta w trakcie
wykonywania programu. Niektóre kompilatory
zwrócą nawet komunikat błędu. Można tę linię
programu wykomentować.
Parametry domyślne
Do każdego zadeklarowanego w prototypie i definicji funkcji parametru,
funkcja wywołująca musi pobrać wartość zgodną z zadeklarowanym typem.
Przykład:
Jeżeli funkcję zadeklarowaną w następujący sposób:
long Funkcja ( int ) ;
to trzeba przekazywać do niej wartość całkowitą. Jeżeli definicja jest
niezgodna z prototypem, lub jeżeli wartość przekazywana nie będzie
całkowita to kompilator zasygnalizuje błąd.
Od tej reguły jest jeden
wyjątek
. Jeżeli w prototypie zadeklarujemy domyślną wartość dla parametru,
to gdy nie określimy danego argumentu, zostanie mu przypisana
automatycznie wartość domyślna. Odpowiednia deklaracja powinna wyglądać
następująco:
long Funkcja ( int x = 10 ) ;
Definicja funkcji nie zmienia się. Nagłówek definicji nadal będzie miał postać:
long Funkcja ( int x )
Teraz, gdy wywoła się funkcję bez określenia wartości argumentu to
kompilator automatycznie nada mu wartość 10. Nazwa parametru z
wartością domyślną nie musi być taka sama jak w nagłówku definicji, wartość
domyślna jest przypisywana na podstawie pozycji w liście parametrów, a nie
według nazwy.
Wartości domyślne można nadawać kilku parametrom funkcji. Jest tu jednak
pewne ograniczenie, jeżeli parametr nie ma określonej wartości domyślnej, to
żaden z poprzedzających go parametrów, również nie może posiadać
wartości domyślnej.
Jeżeli prototyp funkcji wygląda w następujący sposób:
long Funkcja ( int nParam1, int nParam2, int nParam3 ) ;
Przykład:
to parametrowi nParam2 można przypisać wartość domyślną wtedy i tylko
wtedy gdy przypisze się również wartość domyślną do nParam3. Podobnie,
gdy chce się przypisać wartość domyślną do nParam1 to trzeba uwzględnić
parametry nParam2 i nParam3 i również nadać im wartości domyślne.
Deklaracja funkcji ObjProstopadłościanu()
jako funkcję mającą trzy parametry. Dla dwóch
ostatnich są zadane wartości domyślne.
Jeżeli nie poda się nSzerokosci i nWysokosci
to program wykorzysta podane wartości: 10 i
3.
Zdeklarowane i zainicjalizowane zmienne
przekazywane są do funkcji
ObjProstopadlościanu().
Wywołujemy
funkcję
ObjProstopadloscianu(), lecz tym razem bez
parametru nWysokosc. Program wykorzystuje
wartość domyślną (3).
Trzecie
wywołanie
funkcji
ObjProstopadloscianu() Tym razem podany
jest
tylko
jeden
parametr
nDlugosc.
Pozostałym
parametrom
nadawane
są,
wartości domyślne.
Przeciążanie funkcji
C++ pozwala na stworzenie więcej niż jednej funkcji o tej samej nazwie.
Możliwość ta określana jest jako przeciążanie funkcji. Funkcje takie
muszą różnić się listą parametrów, typami parametrów lub ich liczbą.
Przykład:
int FunkcjaNew ( int, int ) ;
int FunkcjaNew ( long, long ) ;
int FunkcjaNew ( long ) ;
FunkcjaNew () jest przeciążona z użyciem trzech rożnych list parametrów.
Dwie pierwsze różnią się typem parametrów, trzecia ma inną ich liczbę.
W przypadku przeciążanych funkcji, typ wartości zwracanej może być
inny lub taki sam. Nie można przeciążać funkcji opierając się jedynie na
typie wartości zwracanej. Trzeba wykorzystać różne listy parametrów.
Przeciążanie funkcji określane jest również mianem
polimorfizmu
funkcji
.
Polimorfizm pozwala na przeciążanie funkcji za pomocą różnych ich treści.
Zmieniając liczbę lub typ parametrów, można nadać funkcjom te same
nazwy. W momencie wywołania, parametry będą decydować, która z funkcji
zostanie wykonana.
Załóżmy, że potrzebujemy funkcję podwajającą dowolną podaną wartość.
Chcielibyśmy mieć możliwość podania zmiennej typu int, long, float i
double.
Przykład:
Bez
przeciążania
funkcji
musiałoby się napisać cztery
funkcje o różnych nazwach:
int PodInt ( int ) ;
long PodLong ( long ) ;
float PodFloat ( float ) ;
doube PodDouble ( double ) ;
Natomiast
z
użyciem
przeciążenia
można
użyć
następującej deklaracji:
int Pod ( int ) ;
long Pod ( long ) ;
float Pod ( float ) ;
double Pod ( double ) ;
Funkcje wewnętrzne (inline)
Gdy definiuje się funkcję, kompilator tworzy w pamięci jeden zestaw
instrukcji. W momencie wywołania funkcji, program przechodzi do wykonania
tych instrukcji. Gdy funkcja kończy swoje działanie, program wraca do
następnej instrukcji po wywołaniu.
Jeżeli funkcja jest zadeklarowana ze słowem kluczowym inline, to
kompilator nie tworzy prawdziwej funkcji. Kopiuje natomiast jej kod w
każde miejsce wywołania. Nie jest wykonywany żaden skok. Wygląda to
tak, jakby fizycznie wpisać treść funkcji zamiast ją wywoływać.
gdy funkcję wywołuje się 10 razy
program tyle samo razy "skoczy" do instrukcji danej
funkcji.
w pamięci istnieje tylko jedna kopia funkcji, (a nie
10).
Prędkość
wykonywania
programu
zwiększa się gdy
unikniemy skoków
do funkcji.
Kiedy zatem stosować funkcje inline?
Wtedy gdy ma się małą funkcję (dwie, trzy linie), to można pomyśleć o
zamienieniu jej na wewnętrzną.
Jeżeli wywoła się taką funkcję dziesięciokrotnie to treść funkcji zostanie
skopiowana w każde z dziesięciu miejsc wywołania. Niewielki wzrost
wydajności może okazać się nieopłacalny w stosunku do wzrostu rozmiaru
kodu wynikowego.
Funkcje inline kosztują (pamięć).
Deklaracja
wygląda
jak
zwykły
prototyp, jedyna różnica polega na
użyciu słowa kluczowego inline. Taka
deklaracja funkcji powoduje, że w
tych miejscach:
nLiczba = 2 * nLiczba ;
nLiczba = 2 * nLiczba ;
nLiczba = 2 * nLiczba ;
Przed
wykonaniem
programu,
kompilator
umieszcza instrukcje funkcji
w kodzie. Oszczędza się w
ten
sposób
na
liczbie
skoków wewnątrz kodu, lecz
traci
na
rozmiarze
programu.
Bliższe spojrzenie na działanie funkcji
Wywołując funkcję, program przechodzi do wykonania instrukcji danej funkcji
i przekazuje parametry. Kiedy funkcja się kończy, zwracana jest wartość
(ewentualnie void) i program wraca do miejsca, z którego funkcja została
wywołana.
Jak to zadanie jest zorganizowane?
Skąd program wie, dokąd ma przejść?
Gdzie są przechowywane zmienne przekazywane do funkcji?
Co się dzieje ze zmiennymi deklarowanymi wewnątrz funkcji?
W jaki sposób jest przekazywana wartość zwracane przez funkcję?
Skąd program wie, dokąd ma wrócić po wykonaniu funkcji?
Stos
Kiedy program rozpoczyna działanie, to kompilator tworzy stos.
Stos to specjalny obszar pamięci (struktura danych) służąca do
przechowywania danych wymaganych przez wszystkie funkcje w
programie. Określenie stos wynika z działania tej struktury, wartość,
która została położona na stos jako ostatnia zostanie zdjęta jako
pierwsza (LIFO ang. Last-in first-out).
Lepiej jest wyobrażać sobie stos jako odpowiedni ciąg komórek pamięci
ustawiony "do góry nogami". Szczyt jest tam gdzie wskazuje wskaźnik stosu.
100
101
102
103
104
105
106
107
108
109
110
80
50
37
Stos
Zmienna
nMojaKasa
nTwojaKasa
poza stosem
na stosie
102
Wskaźnik
stosu
Każda komórka stosu ma swój
adres. Jeden z tych adresów jest
przechowywany
w
rejestrze
stosu. Wszystko poniżej tego
adresu, nazywanego szczytem
stosu, jest traktowane jako
położone na stosie. Wszystko
powyżej jest poza stosem.
100
101
102
103
104
105
106
107
108
109
110
80
50
37
Stos
Zmienna
nMojaKasa
nTwojaKasa
poza stosem
na stosie
108
Wskaźnik
stosu
Kiedy wartość jest odkładana na stos, to jest umieszczana w komórce
powyżej wskaźnika stosu. Wskaźnik stosu jest przesuwany na tą komórkę.
Kiedy wartość jest zdejmowana, to faktycznie zmieniany jest tylko wskaźnik
stosu.
Stos i funkcje
Kiedy program wywołuje funkcję to tworzy dla niej ramkę stosu. Ramka stosu
to obszar na stosie, przeznaczony dla danej funkcji. Jest to bardzo ogólne i
rożnie wykonywane na rożnych komputerach. Można jednak wyróżnić kilka
podstawowych kroków:
Umieść na stosie adres powrotny. Kiedy funkcja się skończy, to
program wróci do tego adresu.
Zrób na stosie miejsca dla zadeklarowanej wartości zwracanej
przez funkcję.
Umieść na stosie argumenty funkcji.
Przejdź do wykonywania funkcji.
Umieść na stosie zmienne lokalne funkcji według ich definicji.