C++1 1, r05-06, Szablon dla tlumaczy


Rozdział 5.
Funkcje

Choć w programowaniu zorientowanym obiektowo zainteresowanie użytkowników zaczęło koncentrować się na obiektach, jednak mimo to funkcje w dalszym ciągu pozostają głównym komponentem każdego programu. Funkcje globalne występują poza obiektami, zaś funkcje składowe (zwane także metodami składowymi) występują wewnątrz obiektów, wykonując ich pracę.

Z tego rozdziału dowiesz się:

Zaczniemy od funkcji globalnych; w następnym rozdziale dowiesz się, w jaki sposób funkcje działają wewnątrz obiektów.

Czym jest funkcja?

Ogólnie, funkcja jest podprogramem, operującym na danych i zwracającym wartość. Każdy program C++ posiada przynajmniej jedną funkcję, main(). Gdy program rozpoczyna działanie, funkcja main() jest wywoływana automatycznie. Może ona wywoływać inne funkcje, które z kolei mogą wywoływać kolejne funkcje.

Ponieważ funkcje te nie stanowią części jakiegoś [Author ID1: at Mon Oct 22 14:46:00 2001 ]obiektu, są nazywane „globalnymi” — mogą być dostępne z dowolnego miejsca programu. W tym rozdziale, gdy będziemy mówić o funkcjach, będziemy mieli na myśli właśnie funkcje globalne (chyba, że postanowimy inaczej).

Każda funkcja posiada nazwę; gdy ta nazwa zostanie napotkana przez program, przechodzi on do wykonywania kodu zawartego wewnątrz ciała tej funkcji. Nazywa się to wywołaniem funkcji. Gdy funkcja wraca, wykonanie programu jest wznawiane od instrukcji następującej po wywołaniu tej funkcji. Ten przepływ sterowania został pokazany na rysunku 5.1.

Rysunek 5.1. Gdy program wywołuje funkcję, sterowanie przechodzi do jej ciała, po czym jest wznawiane od instrukcji występującej po wywołaniu tej funkcji

0x01 graphic

Dobrze zaprojektowane funkcje wykonują określone, łatwo zrozumiałe zadania. Złożone zadania powinny być dzielone na kilka, odpowiednio wywoływanych funkcji.

Funkcje występują w dwóch odmianach: zdefiniowane przez użytkownika (programistę) oraz wbudowane. Funkcje wbudowane stanowią część pakietu dostarczanego wraz z kompilatorem — zostały one stworzone przez producenta kompilatora, z którego korzystasz. Funkcje zdefiniowane przez użytkownika są funkcjami, które piszesz samodzielnie.

Zwracane wartości, parametry i argumenty

Funkcja może zwracać wartość. Gdy wywołujesz funkcję, może ona wykonać swoją pracę, po czym zwrócić wartość stanowiącą rezultat tej pracy. Ta wartość jest nazywana wartością zwracaną, zaś jej typ musi być zadeklarowany. Zatem, gdy piszesz:

int myFunction();

deklarujesz, że funkcja myFunction zwraca wartość całkowitą.

Możesz także przekazywać wartości do funkcji. Te wartości pełnią rolę zmiennych, którymi możesz manipulować wewnątrz funkcji.

Opis przekazywanych wartości jest nazywany listą parametrów.

int myFunction(int someValue, float someFloat);

Ta deklaracja wskazuje, że funkcja myFunction nie tylko zwraca liczbę całkowitą, ale także, że jej parametrami są: wartość całkowita oraz wartość typu float.

Parametr opisuje typ wartości, jaka jest przekazywana funkcji podczas jej wywołania. Wartości przekazywane funkcji są nazywane argumentami.

int theValueReturned = myFunction(5, 6.7);

W tej deklaracji [Author ID1: at Mon Oct 22 14:50:00 2001 ]W[Author ID1: at Mon Oct 22 14:50:00 2001 ]w[Author ID1: at Mon Oct 22 14:50:00 2001 ]idzimy,[Author ID1: at Mon Oct 22 14:50:00 2001 ] tutaj [Author ID1: at Mon Oct 22 14:50:00 2001 ]że zmienna całkowita theValueReturned (zwracana wartość) jest inicjalizowana wartością zwracaną przez funkcję myFunction, której zostały przekazane wartości 5 oraz 6.7 (jako argumenty). Typy argumentów muszą odpowiadać zadeklarowanym typom parametrów.

Deklarowanie i definiowanie funkcji

Aby użyć funkcji w programie, należy najpierw zadeklarować funkcję, a następnie ją zdefiniować. Deklaracja informuje kompilator o nazwie funkcji, typie zwracanej przez nią wartości, oraz o jej parametrach. Z kolei definicja informuje, w jaki sposób dana funkcja działa. Żadna funkcja nie może zostać wywołana z jakiejkolwiek innej funkcji, jeśli nie zostanie wcześniej zadeklarowana. Deklaracja funkcji jest nazywana prototypem.

Deklarowanie funkcji

Istnieją trzy sposoby deklarowania funkcji:

Choć możesz zdefiniować funkcję przed jej użyciem i uniknąć w ten sposób konieczności tworzenia jej prototypu, nie należy to do dobrych obyczajów programistycznych z trzech powodów.

Po pierwsze, niedobrze jest, gdy funkcje muszą występować w pliku źródłowym w określonej kolejności. Powoduje to, że w razie wprowadzenia zmian trudno jest zmodyfikować taki program.

Po drugie, istnieje możliwość, że w pewnych warunkach funkcja A() musi być w stanie wywołać funkcję B(), a funkcja B() także musi być w stanie wywołać funkcję A(). Nie jest możliwe zdefiniowanie funkcji A() przed zdefiniowaniem funkcji B() i jednoczesne zdefiniowanie funkcji B() przed zdefiniowaniem funkcji A(), dlatego przynajmniej jedna z nich zawsze musi zostać zadeklarowana.

Po trzecie, prototypy funkcji stanowią wydajną technikę debuggowania (usuwania błędów w programach). Jeśli z prototypu wynika, że funkcja otrzymuje określony zestaw parametrów lub że zwraca określony typ wartości, to [Author ID1: at Mon Oct 22 14:51:00 2001 ]w przypadku gdy funkcja nie jest zgodna z tym prototypem, kompilator, zamiast czekać na wystąpienie błędu podczas działania programu, może wskazać tę niezgodność. Przypomina to dwustronną księgowość. Prototyp i definicja sprawdzają się wzajemnie, redukując prawdopodobieństwo, że zwykła literówka spowoduje błąd w programie.

Prototypy funkcji

Wiele z wbudowanych funkcji posiada już gotowe prototypy. Występują one w plikach, które są dołączane do programu za pomocą dyrektywy #include. W przypadku funkcji pisanych samodzielnie, musisz stworzyć samodzielnie także ich prototypy.

Prototyp funkcji jest instrukcją, co oznacza, że kończy się on średnikiem. Składa się ze zwracanego przez funkcję typu oraz tzw. sygnatury funkcji. Sygnatura funkcji to jej nazwa oraz lista parametrów.

Lista parametrów jest listą wszystkich parametrów oraz ich typów, oddzielonych od siebie przecinkami. Elementy prototypu funkcji przedstawia rysunek 5.2.

Rysunek 5.2. Elementy prototypu funkcji

0x01 graphic

Zwracany typ oraz sygnatura prototypu i definicji funkcji muszą zgadzać się dokładnie. Jeśli nie są one zgodne, wystąpi błąd kompilacji. Zauważ jednak, że prototyp funkcji nie musi zawierać nazw parametrów, a jedynie ich typy. Poniższy prototyp jest poprawny:

long Area(int, int);

Ten prototyp deklaruje funkcję o nazwie Area (obszar), która zwraca wartość typu long i posiada dwa parametry, będące wartościami całkowitymi. Choć ten zapis jest poprawny, jednak jego stosowanie nie jest dobrym pomysłem. Dodanie nazw parametrów powoduje, że prototyp staje się bardziej czytelny. Ta sama funkcja z nazwanymi parametrami mogłaby być zadeklarowana następująco:

long Area(int length, int width );

W tym przypadku jest oczywiste, do czego służy ta funkcja oraz jakie są jej parametry.

Zwróć uwagę, że wszystkie funkcje zwracają wartość pewnego typu. Jeśli ten typ nie zostanie podany jawnie, zakłada się, że jest wartością całkowitą, a konkretnie typem int. Twoje programy będą jednak łatwiejsze do zrozumienia, jeśli we wszystkich funkcjach, włącznie z funkcją main(), będziesz deklarował zwracany typ.

Definiowanie funkcji

Definicja funkcji składa się z nagłówka funkcji oraz z jej ciała. Nagłówek przypomina prototyp funkcji, w którym wszystkie parametry muszą być nazwane a na końcu nagłówka nie występuje średnik.

Ciało funkcji jest ujętym w nawiasy klamrowe zestawem instrukcji. Rysunek 5.3 przedstawia nagłówek i ciało funkcji.

Rysunek 5.3. Nagłówek i ciało funkcji

0x01 graphic

Listing 5.1 demonstruje program zawierający prototyp oraz deklarację funkcji Area().

Listing 5.1. Deklaracja i definicja funkcji oraz ich wykorzystanie w programie

0: // Listing 5.1 - demonstruje użycie prototypów funkcji

1:

2: #include <iostream>

3: int Area(int length, int width); //prototyp funkcji

4:

5: int main()

6: {

7: using std::cout;

8: using std::cin;

9:

10: int lengthOfYard;

11: int widthOfYard;

12: int areaOfYard;

13:

14: cout << "\nJak szerokie jest twoje podworko? ";

15: cin >> widthOfYard;

16: cout << "\nJak dlugie jest twoje podworko? ";

17: cin >> lengthOfYard;

18:

19: areaOfYard= Area(lengthOfYard,widthOfYard);

20:

21: cout << "\nTwoje podworko ma ";

22: cout << areaOfYard;

23: cout << " metrow kwadratowych\n\n";

24: return 0;

25: }

26:

27: int Area(int l, int w)

28: {

29: return l * w;

30: }

Wynik

Jak szerokie jest twoje podworko? 100

Jak dlugie jest twoje podworko? 200

Twoje podworko ma 20000 metrow kwadratowych

Analiza

Prototyp funkcji Area() znajduje się w linii 3. Porównaj ten prototyp z definicją funkcji, zaczynającą się od linii 27. Zwróć uwagę, że nazwa, zwracany typ oraz typy parametrów są takie same. Gdyby były różne, wystąpiłby błąd kompilacji. W rzeczywistości jedyną różnicę stanowi fakt, że prototyp funkcji kończy się średnikiem i nie posiada ciała.

Zwróć także uwagę, że nazwy parametrów w prototypie to length (długość) oraz width (szerokość), ale nazwy parametrów w definicji to l oraz w. Jak wspomniano wcześniej, nazwy w prototypie nie są używane i służą wyłącznie jako informacja dla programisty. Do dobrych obyczajów programistycznych należy dopasowanie nazw parametrów prototypu do nazw parametrów definicji (nie jest to wymaganie języka).

Argumenty są przekazywane funkcji w takiej kolejności, w jakiej zostały zadeklarowane i zdefiniowane parametry, lecz ich nazwy nie muszą do siebie pasować. Gdy przekażesz zmienną widthOfYard (szerokość podwórka), a po niej lengthOfYard (długość podwórka), wtedy funkcja FindArea (znajdź obszar) użyje wartości widthOfYard jako długości oraz wartości lengthOfYard jako szerokości. Ciało funkcji jest zawsze ujęte w nawiasy klamrowe, nawet jeśli (tak jak w tym przypadku) składa się z jednej tylko instrukcji.

Wykonywanie funkcji

Gdy wywołujesz funkcję, jej wykonanie rozpoczyna się od pierwszej instrukcji następującej po otwierającym nawiasie klamrowym ({). Rozgałęzienie działania można uzyskać za pomocą instrukcji if. (Instrukcja if oraz instrukcje z nią związane zostaną omówione w rozdziale 7.). Funkcje mogą także wywoływać inne funkcje, a nawet wywoływać siebie same (patrz podrozdział „Rekurencja” w dalszej części tego rozdziału).

Zmienne lokalne

Zmienne można przekazywać funkcjom; można również deklarować zmienne wewnątrz ciała funkcji. Zmienne deklarowane wewnątrz ciała funkcji są nazywane „lokalnymi”, gdyż istnieją tylko lokalnie wewnątrz danej funkcji. Gdy funkcja wraca (kończy działanie), zmienne lokalne przestają być dostępne i zostają zniszczone przez kompilator.

Zmienne lokalne są definiowane tak samo, jak wszystkie inne zmienne. Parametry przekazywane do funkcji także są uważane za zmienne lokalne i mogą być używane identycznie, jak zmienne zadeklarowane wewnątrz ciała funkcji. Przykład użycia parametrów oraz zmiennych zadeklarowanych lokalnie wewnątrz funkcji przedstawia listing 5.2.

Listing 5.2. Użycie zmiennych lokalnych oraz parametrów

0: #include <iostream>

1:

2: float Convert(float);

3: int main()

4: {

5: using namespace std;

6:

7: float TempFer;

8: float TempCel;

9:

10: cout << "Podaj prosze temperature w stopniach Fahrenheita: ";

11: cin >> TempFer;

12: TempCel = Convert(TempFer);

13: cout << "\nOdpowiadajaca jej temperatura w stopniach Celsjusza: ";

14: cout << TempCel << endl;

15: return 0;

16: }

17:

18: float Convert(float TempFer)

19: {

20: float TempCel;

21: TempCel = ((TempFer - 32) * 5) / 9;

22: return TempCel;

23: }

Wynik

Podaj prosze temperature w stopniach Fahrenheita: 212

Odpowiadajaca jej temperatura w stopniach Celsjusza: 100

Podaj prosze temperature w stopniach Fahrenheita: 32

Odpowiadajaca jej temperatura w stopniach Celsjusza: 0

Podaj prosze temperature w stopniach Fahrenheita: 85

Odpowiadajaca jej temperatura w stopniach Celsjusza: 29.4444

Analiza

W liniach 7. i 8. są deklarowane dwie zmienne typu float, z których jednak[Author ID1: at Mon Oct 22 14:52:00 2001 ] przechowuje temperaturę w stopniach Fahrenheita, zaś druga w stopniach Celsjusza. W linii 10. użytkownik jest proszony o podanie temperatury w stopniach Fahrenheita, zaś uzyskana wartość jest przekazywana funkcji Convert() (konwertuj).

Wykonanie programu przechodzi do pierwszej linii funkcji Convert() w linii 20., w której jest deklarowana zmienna lokalna, także o nazwie TempCel (temperatura w stopniach Celsjusza). Zwróć uwagę, że ta zmienna lokalna nie jest równoważna zmiennej TempCel w linii 8. Ta zmienna istnieje tylko wewnątrz funkcji Convert(). Wartość przekazywana jako parametr, TempFer (temperatura w stopniach Fahrenheita), także jest tylko lokalną kopią zmiennej przekazywanej przez funkcję main().

Ta funkcja mogłaby posiadać parametr o nazwie FerTemp i zmienną lokalną CelTemp, a program działałby równie dobrze. Aby przekonać się że program działa, możesz wpisać te nazwy i ponownie go skompilować.

Lokalnej zmiennej[Author ID1: at Mon Oct 22 14:52:00 2001 ]a[Author ID1: at Mon Oct 22 14:52:00 2001 ] funkcji, TempCel, jest przypisywana wartość, będąca wynikiem odjęcia 32 od parametru TempFer, pomnożenia przez 5, a następnie podzielenia przez 9. Ta wartość jest następnie zwracana jako wartość funkcji, która w linii 12. jest przypisywana zmiennej TempCel wewnątrz funkcji main(). Ta wartość jest wypisywana w linii 14.

Program został uruchomiony trzykrotnie. Za pierwszym razem została podana wartość 212, w celu upewnienia się, czy punkt wrzenia wody w stopniach Fahrenheita (212) daje właściwy wynik w stopniach Celsjusza (100). Drugi test sprawdza temperaturę zamarzania wody. Trzeci test to przypadkowa wartość, wybrana w celu wygenerowania wyniku ułamkowego.

Zakres

Zmienna posiada zakres, który określa, jak długo i w których miejscach programu jest ona dostępna. Zmienne zadeklarowane wewnątrz bloku mają zakres obejmujący ten blok; mogą być dostępne tylko wewnątrz tego bloku i „przestają istnieć” po wyjściu programu z tego bloku. Zmienne globalne mają zakres globalny i są dostępne w każdym miejscu programu.

Zmienne globalne

Zmienne zdefiniowane poza funkcją mają zakres globalny i są dostępne z każdej funkcji w programie, włącznie z funkcją main().

Zmienne lokalne o takich samych nazwach, jak zmienne globalne nie zmieniają zmiennych globalnych. Jednak zmienna lokalna o takiej samej nazwie, jak zmienna globalna przesłania [Author ID1: at Mon Oct 22 14:53:00 2001 ]ukrywa [Author ID1: at Mon Oct 22 14:53:00 2001 ]zmienną globalną. Jeśli funkcja posiada zmienną o takiej samej nazwie jak zmienna globalna, to [Author ID1: at Mon Oct 22 14:54:00 2001 ]ta [Author ID1: at Mon Oct 22 14:54:00 2001 ]nazwa użyta wewnątrz funkcji odnosi się do zmiennej lokalnej, a nie do globalnej. Ilustruje to listing 5.3.

Listing 5.3. Przykład zmiennych lokalnych i globalnych

0: #include <iostream>

1: void myFunction(); // prototyp

2:

3: int x = 5, y = 7; // zmienne globalne

4: int main()

5: {

6: using std::cout;

7:

8: cout << "x z funkcji main: " << x << "\n";

9: cout << "y z funkcji main: " << y << "\n\n";

10: myFunction();

11: cout << "Wrocilem z myFunction!\n\n";

12: cout << "x z funkcji main: " << x << "\n";

13: cout << "y z funkcji main: " << y << "\n";

14: return 0;

15: }

16:

17: void myFunction()

18: {

19: using std::cout;

20:

21: int y = 10;

22:

23: cout << "x z funkcji myFunction: " << x << "\n";

24: cout << "y z funkcji myFunction: " << y << "\n\n";

25: }

Wynik

x z funkcji main: 5

y z funkcji main: 7

x z funkcji myFunction: 5

y z funkcji myFunction: 10

Wrocilem z myFunction!

x z funkcji main: 5

y z funkcji main: 7

Analiza

Ten prosty program ilustruje kilka kluczowych i potencjalnie niezrozumiałych zagadnień, dotyczących zmiennych lokalnych i globalnych. W linii 3. są deklarowane dwie zmienne globalne, x oraz y. Zmienna globalna x jest inicjalizowana wartością 5, zaś zmienna globalna y jest inicjalizowana wartością 7.

W liniach 8. i 9. w funkcji main() te wartości są wypisywane na ekranie. Zauważ, że funkcja main() nie definiuje tych zmiennych; ponieważ są one globalne, są one dostępne w funkcji main().

Gdy w linii 10. zostaje wywołana funkcja myFunction(), działanie programu przechodzi do linii 17. a w linii 21. jest definiowana lokalna zmienna y, inicjalizowana wartością 10. W linii 23. funkcja myFunction() wypisuje wartość zmiennej x; w tym przypadku zostaje użyta wartość globalnej zmiennej x, tak jak w funkcji main(). Jednak w linii 24., w której zostaje użyta nazwa zmiennej y, wykorzystywana jest lokalna zmienna y, gdyż przesłoniła ([Author ID1: at Mon Oct 22 14:54:00 2001 ]ukryła)[Author ID1: at Mon Oct 22 14:54:00 2001 ] ona zmienną globalną o tej samej nazwie.

Funkcja kończy swoje działanie i zwraca sterowanie do funkcji main(), która ponownie wypisuje wartości zmiennych globalnych. Zauważ, że przypisanie wartości do zmiennej lokalnej y w funkcji myFunction() w żaden sposób nie wpłynęło na wartość globalnej zmiennej y.

Zmienne globalne: ostrzeżenie

W C++ dozwolone są zmienne globalne, ale prawie nigdy nie są one używane. C++ wyrosło z języka C, zaś w tym języku zmienne globalne były niebezpiecznym, choć niezbędnym narzędziem. Zmienne globalne są konieczne, gdyż zdarzają się sytuacje, w których dane muszą być łatwo dostępne dla wielu funkcji i nie chcemy ich przekazywać z funkcji do funkcji w postaci parametrów.

Zmienne globalne są niebezpieczne, gdyż zawierają wspólne dane, które mogą być zmienione przez którąś z funkcji w sposób niewidoczny dla innych. Może to powodować bardzo trudne do odszukania błędy.

W rozdziale 15., „Specjalne klasy i funkcje,” poznasz alternatywę dla zmiennych globalnych, stanowią ją statyczne zmienne składowe.

Kilka słów na temat zmiennych lokalnych

Zmienne można definiować w dowolnym miejscu funkcji, nie tylko na jej początku. Zakresem zmiennej jest blok, w którym została zdefiniowana. Dlatego, jeśli zdefiniujesz zmienną wewnątrz nawiasów klamrowych wewnątrz funkcji, będzie ona dostępna tylko wewnątrz tego bloku. Ilustruje to listing 5.4.

Listing 5.4. Zakres zmiennych ogranicza się do bloku, w którym zostały zadeklarowane

0: // Listing 5.4 - demonstruje że zakres zmiennej

1: // ogranicza się do bloku, w którym została zadeklarowana

2:

3: #include <iostream>

4:

5: void myFunc();

6:

7: int main()

8: {

9: int x = 5;

10: std::cout << "\nW main x ma wartosc: " << x;

11:

12: myFunc();

13:

14: std::cout << "\nPonownie w main, x ma wartosc: " << x;

15: return 0;

16: }

17:

18: void myFunc()

19: {

20: int x = 8;

21: std::cout << "\nW myFunc, lokalne x: " << x << std::endl;

22:

23: {

24: std::cout << "\nW bloku w myFunc, x ma wartosc: " << x;

25:

26: int x = 9;

27:

28: std::cout << "\nBardzo lokalne x: " << x;

29: }

30:

31: std::cout << "\nPoza blokiem, w myFunc, x: " << x << std::endl;

32: }

Wynik

W main x ma wartosc: 5

W myFunc, lokalne x: 8

W bloku w myFunc, x ma wartosc: 8

Bardzo lokalne x: 9

Poza blokiem, w myFunc, x: 8

Ponownie w main, x ma wartosc: 5

Analiza

Ten program zaczyna działanie od inicjalizacji lokalnej zmiennej [Author ID1: at Mon Oct 22 14:55:00 2001 ]x[Author ID1: at Mon Oct 22 14:55:00 2001 ] [Author ID1: at Mon Oct 22 14:55:00 2001 ]w linii 9.,[Author ID1: at Mon Oct 22 14:56:00 2001 ] lokalnej zmiennej [Author ID1: at Mon Oct 22 14:55:00 2001 ]x[Author ID1: at Mon Oct 22 14:55:00 2001 ] [Author ID1: at Mon Oct 22 14:55:00 2001 ]w funkcji main(). Komunikat wypisywany w linii 10. potwierdza, że x zostało zainicjalizowane wartością 5.

Wywoływana jest funkcja myFunc(), w której, w linii 20., wartością 8 jest inicjalizowana lokalna zmienna, także o nazwie x. Jej wartość jest wypisywana w linii 21.

W linii 23. rozpoczyna się blok, a w linii 24. ponownie wypisywana jest wartość zmiennej x z funkcji. Wewnątrz bloku, w linii 26., tworzona jest nowa zmienna, także o nazwie x, która jednak jest lokalna dla bloku. Jest ona inicjalizowana wartością 9.

Wartość najnowszej zmiennej jest wypisywana w linii 28. Lokalny blok kończy się w linii 29., gdzie zmienna stworzona w linii 26. „wychodzi” z zakresu i nie jest już widoczna.

Gdy w linii 31. jest wypisywana wartość x, jest to wartość zmiennej x zadeklarowanej w linii 20. Na tę zmienną nie miała wpływu deklaracja zmiennej x w linii 26.; jej wartość wynosi wciąż 8.

W linii 32., funkcja myFunc() wychodzi z zakresu, a jej zmienna lokalna x staje się niedostępna. Wykonanie wraca do linii 14., w której jest wypisywana wartość lokalnej zmiennej x, stworzonej w linii 9. Nie ma na nią wpływu żadna ze zmiennych zdefiniowanych w funkcji myFunc().

Mimo wszystko, program ten sprawiałby dużo mniej kłopotów, gdyby te trzy zmienne posiadały różne nazwy!

Instrukcje funkcji

Praktycznie ilość rodzajów instrukcji, które mogą być umieszczone wewnątrz ciała funkcji, jest nieograniczona. Choć wewnątrz danej funkcji nie można definiować innych funkcji, jednak można je wywoływać, z czego korzysta oczywiście funkcja main() w większości programów C++. Funkcje mogą nawet wywoływać same siebie (co zostanie wkrótce omówione w podrozdziale poświęconym rekurencji).

Chociaż rozmiar funkcji w języku C++ nie jest ograniczony, jednak dobrze zaprojektowane funkcje są zwykle niewielkie. Wielu programistów radzi, by funkcja mieściła się na pojedynczym ekranie tak, aby można ją było widzieć w całości. Ta reguła jest często łamana, także przez bardzo dobrych programistów. Jednakże mniejsze funkcje są łatwiejsze do zrozumienia i utrzymania.

Każda funkcja powinna spełniać[Author ID1: at Mon Oct 22 14:56:00 2001 ] pojedyncze, dobrze określone zadanie. Jeśli funkcja zbytnio się rozrasta, poszukaj miejsc, w których możesz podzielić ją na mniejsze podzadania.

Kilka słów na temat argumentów funkcji

Argumenty funkcji nie muszą być tego samego typu. Najzupełniej poprawne i sensowne jest na przykład posiadanie funkcji, której argumentami są liczba całkowita, dwie liczby typu long oraz znak (char)[Author ID1: at Mon Oct 22 14:56:00 2001 ].

Argumentem funkcji może być każde poprawne wyrażenie języka C++, łącznie ze stałymi, wyrażeniami matematycznymi i logicznymi, a także innymi funkcjami zwracającymi wartość.

Użycie funkcji jako parametrów funkcji

Choć jest dozwolone, by parametrem funkcji była inna zwracająca wartość funkcja, może to spowodować trudności w debuggowaniu kodu i jego nieczytelność.

Na przykład, przypuśćmy, że masz funkcje: myDouble() (podwojenie), triple() (potrojenie), square() (do kwadratu) oraz cube() (do trzeciej potęgi), z których każda zwraca wartość. Mógłbyś napisać:

Answer = (myDouble(triple(square(cube(myValue)))));

Ta instrukcja pobiera zmienną, myValue i przekazuje ją jako argument do funkcji cube(), której zwracana wartość jest przekazywana jako argument do funkcji square(), której zwracana wartość jest przekazywana z kolei jako argument do funkcji triple(), zaś jej zwracana wartość jest przekazywana do funkcji myDouble(). Ostatecznie zwracana wartość tej funkcji jest przypisywana zmiennej Answer (odpowiedź).

Trudno jest przewidzieć, do czego służy ten kod (wartość jest potrajana przed, czy po podniesieniu do kwadratu?), a gdy odpowiedź jest niepoprawna, trudno będzie sprawdzić, która z funkcji działa niewłaściwie.

Alternatywą jest użycie w każdym kroku oddzielnej, pośredniej zmiennej:

unsigned long myValue = 2;

unsigned long cubed = cube(myValue); // cubed = 8

unsigned long squared = square(cubed); // squared = 64

unsigned long tripled = triple(squared); // tripled = 192

unsigned long Answer = myDouble(tripled); // Answer = 384

Teraz każdy pośredni wynik może zostać sprawdzony, zaś kolejność wykonywania jest bardzo dobrze widoczna.

Parametry są zmiennymi lokalnymi

Argumenty przekazywane funkcji są lokalne dla tej funkcji. Zmiany dokonane w argumentach nie wpływają na wartości w funkcji wywołującej. Nazywa się to przekazywaniem przez wartość, co oznacza, że wewnątrz funkcji jest tworzona lokalna kopia każdego z argumentów. Te lokalne kopie są traktowane tak samo, jak każda inna zmienna lokalna. Ilustruje to listing 5.5.

Listing 5.5. Przykład przekazywania przez wartość

0: // Listing 5.5 - demonstracja przekazywania przez wartość.

1:

2: #include <iostream>

3:

4: void swap(int x, int y);

5:

6: int main()

7: {

8: int x = 5, y = 10;

9:

10: std::cout << "Funkcja Main. Przed funkcja Swap, x: " << x << " y: " << y << "\n";

11: swap(x,y);

12: std::cout << "Funkcja Main. Po funkcji Swap, x: " << x << " y: " << y << "\n";

13: return 0;

14: }

15:

16: void swap (int x, int y)

17: {

18: int temp;

19:

20: std::cout << "Funkcja Swap. Przed zamiana, x: " << x << " y: " << y << "\n";

21:

22: temp = x;

23: x = y;

24: y = temp;

25:

26: std::cout << "Funkcja Swap. Po zamianie, x: " << x << " y: " << y << "\n";

27: }

Wynik

Funkcja Main. Przed funkcja Swap, x: 5 y: 10

Funkcja Swap. Przed zamiana, x: 5 y: 10

Funkcja Swap. Po zamianie, x: 10 y: 5

Funkcja Main. Po funkcji Swap, x: 5 y: 10

Analiza

Program inicjalizuje w funkcji main() dwie zmienne, po czym przekazuje je do funkcji swap() (zamień), która wydaje się wzajemnie wymieniać ich wartości. Jednak gdy ponownie sprawdzimy ich wartość w funkcji main(), okazuje się, że pozostały niezmienione!

Zmienne są inicjalizowane w linii 8., a ich wartości są pokazywane w linii 10. Następnie wywoływana jest funkcja swap(), do której przekazywane są zmienne.

Działanie programu przechodzi do funkcji swap(), gdzie w linii 20 wartości są wypisywane ponownie. Mają tę samą kolejność, jak w funkcji main(), czego zresztą oczekiwaliśmy. W liniach od 22. do 24. wartości są zamieniane, co potwierdza komunikat wypisywany w linii 26. Rzeczywiście, wewnątrz funkcji swap() wartości zostały zamienione.

Wykonanie programu powraca następnie do linii 12., z powrotem do funkcji main(), gdzie okazuje się, że wartości nie są już zamienione.

Jak zapewne się domyślasz, wartości przekazane funkcji swap() zostały przekazane przez wartość, co oznacza, że w tej funkcji zostały utworzone ich lokalne kopie. Właśnie te zmienne lokalne są zamieniane w liniach od 22. do 24., bez odwoływania się do zmiennych funkcji main().

W rozdziale 8., „Wskaźniki,” oraz rozdziale 10., „Funkcje zaawansowane,” poznasz alternatywne sposobyę[Author ID1: at Mon Oct 22 14:59:00 2001 ] [Author ID1: at Mon Oct 22 14:59:00 2001 ]przekazywania przez wartość, które[Author ID1: at Mon Oct 22 14:59:00 2001 ]a[Author ID1: at Mon Oct 22 14:59:00 2001 ] umożliwią[Author ID1: at Mon Oct 22 14:59:00 2001 ]łaby[Author ID1: at Mon Oct 22 14:59:00 2001 ] zmianę wartości przekazanych prze[Author ID1: at Mon Oct 22 14:59:00 2001 ]z [Author ID1: at Mon Oct 22 15:00:00 2001 ]w [Author ID1: at Mon Oct 22 15:00:00 2001 ]funkcji[Author ID1: at Mon Oct 22 15:00:00 2001 ]ę[Author ID1: at Mon Oct 22 15:00:00 2001 ] main().

Kilka słów na temat zwracanych wartości

Funkcja zwraca albo wartość, albo typ void (pusty). Typ void jest dla kompilatora sygnałem, że funkcja nie zwraca żadnej wartości.

Aby zwrócić wartość z funkcji, użyj słowa kluczowego return, a po nim wartości, którą chcesz zwrócić. Wartość ta może być wyrażeniem zwracającym wartość. Na przykład:

return 5;

return (x > 5);

return (MyFunction());

Wszystkie te instrukcje są poprawne, pod warunkiem, że funkcja MyFunction() także zwraca wartość. Wartością w drugiej instrukcji, return (x > 5); będzie false, gdy x nie jest większe od 5, lub true w odwrotnej sytuacji. Zwracana jest wartość wyrażenia, false lub true, a nie wartość x.

Gdy program natrafia na słowo kluczowe return, następująca po nim wartość jest zwracana jako wartość funkcji. Wykonanie programu powraca natychmiast do funkcji wywołującej, zaś instrukcje występujące po instrukcji return nie są już wykonywane.

Pojedyncza funkcja może zawierać więcej niż jedną instrukcję return. Ilustruje to listing 5.6.

Listing 5.6. Przykład kilku instrukcji return zawartych w tej samej funkcji

0: // Listing 5.6 - Demonstracja kilku instrukcji return

1: // zawartych w tej samej funkcji.

2:

3: #include <iostream>

4:

5: int Doubler(int AmountToDouble);

6:

7: int main()

8: {

9: using std::cout;

10:

11: int result = 0;

12: int input;

13:

14: cout << "Wpisz liczbe do podwojenia (od 0 do 10 000): ";

15: std::cin >> input;

16:

17: cout << "\nPrzed wywolaniem funkcji Doubler... ";

18: cout << "\nwejscie: " << input << " podwojone: " << result << "\n";

19:

20: result = Doubler(input);

21:

22: cout << "\nPo powrocie z funkcji Doubler...\n";

23: cout << "\nwejscie: " << input << " podwojone: " << result << "\n";

24:

25: return 0;

26: }

27:

28: int Doubler(int original)

29: {

30: if (original <= 10000)

31: return original * 2;

32: else

33: return -1;

34: std::cout << "Nie mozesz tu byc!\n";

35: }

Wynik

Wpisz liczbe do podwojenia (od 0 do 10 000): 9000

Przed wywolaniem funkcji Doubler...

wejscie: 9000 podwojone: 0

Po powrocie z funkcji Doubler...

wejscie: 9000 podwojone: 18000

Wpisz liczbe do podwojenia (od 0 do 10 000): 11000

Przed wywolaniem funkcji Doubler...

wejscie: 11000 podwojone: 0

Po powrocie z funkcji Doubler...

wejscie: 11000 podwojone: -1

Analiza

W liniach 14. i 15. program prosi o podanie liczby, która jest wypisywana w linii 18., razem z wynikiem w zmiennej lokalnej. Następnie w linii 20. jest wywoływana funkcja Doubler() (podwojenie), której argumentem jest zmienna input (wejście). Rezultat jest przypisywany lokalnej zmiennej result (wynik), a w linii 23. ponownie wypisywane są wartości.

W linii 30., w funkcji Doubler(), następuje sprawdzenie, czy parametr jest większy od 10000. Jeśli nie, funkcja zwraca podwojoną liczbę pierwotną. Jeśli jest większy od 10000, funkcja zwraca -1 jako wartość błędu.

Instrukcja w linii 34. nigdy nie jest wykonywana, ponieważ bez względu na to, czy wartość jest [Author ID1: at Mon Oct 22 15:01:00 2001 ]większa od 10000, czy nie, funkcja wraca (do [Author ID1: at Mon Oct 22 15:00:00 2001 ]main[Author ID1: at Mon Oct 22 15:00:00 2001 ]) [Author ID1: at Mon Oct 22 15:00:00 2001 ]w linii 31. lub 33. — czyli przed przejściem do linii 34. Dobry kompilator ostrzeże, że ta instrukcja nie może zostać wykonana, zaś[Author ID1: at Mon Oct 22 15:01:00 2001 ]a dobry programista ją usunie[Author ID1: at Mon Oct 22 15:01:00 2001 ]![Author ID1: at Mon Oct 22 15:02:00 2001 ]powinien się tym zająć![Author ID1: at Mon Oct 22 15:01:00 2001 ]

Często zadawane pytanie

Jaka jest różnica pomiędzy int main() a void main(); której formy powinienem użyć? Używałem obu i obie działają poprawnie, dlaczego więc powinienem używać int main(){ return 0;}?

Odpowiedź: W większości kompilatorów działają obie formy, ale zgodna z ANSI jest tylko forma int main() i tylko jej użycie gwarantuje, że program będzie mógł być bez zmian kompilowany także w przyszłości.

Oto różnica: int main() zwraca wartość do systemu operacyjnego. Gdy program kończy działanie, ta wartość może być odczytana przez, na przykład, program wsadowy.

Nie będziemy używać tej zwracanej wartości (rzadko się z niej korzysta), ale wymaga jej standard ANSI.

Parametry domyślne

Funkcja wywołująca musi przekazać wartość dla każdego parametru zadeklarowanego w prototypie i definicji funkcji. Przekazywana wartość musi być zgodna z zadeklarowanym typem. Zatem, gdy masz funkcję zadeklarowaną jako

long myFunction(int);

wtedy funkcja ta musi otrzymać wartość całkowitą. Jeśli definicja funkcji jest inna lub nie przekażesz jej wartości całkowitej, wystąpi błąd kompilacji.

Jedyny wyjątek od tej reguły obowiązuje, gdy prototyp funkcji deklaruje domyślną wartość parametru. Ta domyślna wartość jest używana wtedy, gdy nie zostanie przekazany argument funkcji. Poprzednią deklarację można przepisać jako

long myFunction (int x = 50);

Ten prototyp informuje, że funkcja myFunction() zwraca wartość typu long i otrzymuje parametr będący wartością całkowitą. Jeśli argument nie zostanie podany, użyta zostanie domyślna wartość 50. Ponieważ nazwy parametrów nie są wymagane w prototypach funkcji, tę deklarację można zapisać następująco:

long myFunction (int = 50);

Definicja funkcji nie zmienia się w wyniku zadeklarowania parametru domyślnego. W tym przypadku nagłówek definicji funkcji przyjmie postać:

long myFunction (int x)

Jeśli funkcja wywołująca nie przekaże parametru, kompilator wypełni parametr x domyślną wartością 50. Nazwa domyślnego parametru w prototypie nie musi być tak sama, jak nazwa w nagłówku funkcji; domyślna wartość jest przypisywana na podstawie pozycji, a nie nazwy.

Wartość domyślna może zostać przypisana każdemu parametrowi funkcji. Istnieje tylko jedno ograniczenie: jeśli któryś z parametrów nie ma wartości domyślnej, nie może jej mieć także żaden z wcześniejszych parametrów.

Jeśli prototyp funkcji ma postać:

long myFunction (int Param1, int Param2, int Param3);

to parametrowi Param1 możesz przypisać domyślną wartość tylko wtedy, gdy przypiszesz ją również parametrom Param2 i Param3. Użycie parametrów domyślnych ilustruje listing 5.7.

Listing 5.7. Użycie parametrów domyślnych

0: // Listing 5.7 - demonstruje użycie

1: // domyślnych wartości parametrów

2:

3: #include <iostream>

4:

5: int VolumeCube(int length, int width = 25, int height = 1);

6:

7: int main()

8: {

9: int length = 100;

10: int width = 50;

11: int height = 2;

12: int area;

13:

14: area = VolumeCube(length, width, height);

15: std::cout << "Za pierwszym razem objetosc wynosi: " << area << "\n";

16:

17: area = VolumeCube(length, width);

18: std::cout << "Za drugim razem objetosc wynosi: " << area << "\n";

19:

20: area = VolumeCube(length);

21: std::cout << "Za trzecim razem objetosc wynosi: " << area << "\n";

22: return 0;

23: }

24:

25: int VolumeCube(int length, int width, int height)

26: {

27:

28: return (length * width * height);

29: }

Wynik

Za pierwszym razem objetosc wynosi: 10000

Za drugim razem objetosc wynosi: 5000

Za trzecim razem objetosc wynosi: 2500

Analiza

W linii 5., prototyp funkcji VolumeCube() (objętość sześcianu) określa, że ta funkcja posiada trzy parametry, będące wartościami całkowitymi. Dwa ostatnie posiadają wartości domyślne.

Ta funkcja oblicza objętość sześcianu, którego wymiary zostały jej przekazane. Jeśli nie zostanie podana szerokość (width), funkcja użyje szerokości równej 25 i wysokości (height) równej 1. Jeśli zostanie podana szerokość, lecz nie zostanie podana wysokość, funkcja użyje wysokości równej 1. Nie ma możliwości przekazania wysokości bez przekazania szerokości.

W liniach od 9. do 11. inicjalizowane są wymiary, a w linii 14. są one przekazywane funkcji VolumeCube(). Obliczana jest objętość, zaś wynik jest wypisywany w linii 15.

Wykonanie przechodzi[Author ID1: at Mon Oct 22 15:02:00 2001 ]powraca[Author ID1: at Mon Oct 22 15:03:00 2001 ] do linii 17., w której ponownie wywoływana jest funkcja VolumeCube() (lecz tym razem bez podawania wysokości). Używana jest wartość domyślna, a objętość jest ponownie obliczana i wypisywana.

Wykonanie przechodzi[Author ID1: at Mon Oct 22 15:03:00 2001 ]powraca[Author ID1: at Mon Oct 22 15:03:00 2001 ] do linii 20., lecz tym razem nie jest przekazywana ani szerokość, ani wysokość. Wykonanie po raz trzeci p[Author ID1: at Mon Oct 22 15:04:00 2001 ]rzechodzi [Author ID1: at Mon Oct 22 15:04:00 2001 ]powraca[Author ID1: at Mon Oct 22 15:04:00 2001 ] do linii 25. Użyte zostają domyślne wartości. Obliczana i wypisywana jest objętość.

TAK

NIE

Pamiętaj, że parametry funkcji pełnią wewnątrz tej funkcji rolę zmiennych lokalnych.

Nie próbuj tworzyć domyślnej wartości dla pierwszego parametru, jeśli nie istnieje domyślna wartość dla drugiego.

Nie zapominaj, że argumenty przekazywane przez wartość nie wpływają na zmienne w funkcji wywołującej.

Nie zapominaj, że zmiana zmiennej globalnej w jednej z funkcji zmienia jej wartość we wszystkich funkcjach.

Przeciążanie funkcji

C++ umożliwia tworzenie większej ilości funkcji o tej samej nazwie. Nazywa się to przeciążaniem lub przeładowaniem [Author ID1: at Mon Oct 22 15:05:00 2001 ]funkcji (ang. function overloading). Listy parametrów funkcji [Author ID1: at Mon Oct 22 15:06:00 2001 ]przeciążonych funkcji [Author ID1: at Mon Oct 22 15:06:00 2001 ]muszą się [Author ID1: at Mon Oct 22 15:06:00 2001 ]różnić się [Author ID1: at Mon Oct 22 15:06:00 2001 ]od siebie albo typami parametrów, albo ich ilością, albo jednocześnie typami i ilością. Oto przykład:

int myFunction (int, int);

int myFunction (long, long);

int myFunction (long);

Funkcja myFunction() jest przeciążona z [Author ID1: at Mon Oct 22 15:07:00 2001 ]trzema listami parametrów. Pierwsza i druga wersja różnią się od siebie typem parametrów, zaś trzecia wersja różni się od nich ilością parametrów.

Typy wartości zwracanych przez funkcje [Author ID1: at Mon Oct 22 15:07:00 2001 ]przeciążone funkcje [Author ID1: at Mon Oct 22 15:07:00 2001 ]mogą być takie same lub różne.

UWAGA Dwie funkcje o tych samych nazwach i listach parametrów, różniące się tylko typem zwracanej wartości, powodują wystąpienie błędu kompilacji. Aby zmienić zwracany typ, musisz zmienić także sygnaturę funkcji (tj. jej nazwę i (lub) listę parametrów).

Przeciążanie funkcji zwane jest także polimorfizmem funkcji. „Poli” oznacza „wiele”, zaś „morf” oznacza „formę”; tak więc „polimorfizm” oznacza „wiele form”.

Polimorfizm funkcji oznacza możliwość „przeciążenia” funkcji więcej niż jednym znaczeniem. Zmieniając ilość lub typ parametrów, możemy nadawać jednej lub więcej funkcjom tę samą nazwę, a mimo to, na podstawie użytych parametrów, zostanie wywołana właściwa funkcja. Dzięki temu można na przykład tworzyć funkcje uśredniające liczby całkowite, zmiennoprzecinkowe i inne wartości bez konieczności tworzenia osobnych nazw dla każdej funkcji, np. AverageInts() (uśredniaj wartości całkowite), AverageDoubles() (uśredniaj wartości [Author ID1: at Mon Oct 22 15:08:00 2001 ]typu[Author ID1: at Mon Oct 22 15:08:00 2001 ] double), itd.

Przypuśćmy, że piszesz funkcję, która podwaja każdą wartość, którą jej przekażesz. Chciałbyś mieć możliwość przekazywania jej wartości typu int, long, float oraz double. Bez przeciążania funkcji musiałbyś wymyślić cztery jej nazwy:

int DoubleInt(int);

long DoubleLong(long);

float DoubleFloat(float);

double DoubleDouble(double);

Dzięki przeciążaniu funkcji możesz zastosować deklaracje:

int Double(int);

long Double(long);

float Double(float);

double Double(double);

Są one łatwiejsze do odczytania i wykorzystania. Nie musisz pamiętać, którą funkcję należy wywołać; po prostu przekazujesz jej zmienną, a właściwa funkcja zostaje wywołana automatycznie. Takie zastosowanie przeciążania funkcji przedstawia listing 5.8.

Listing 5.8. Przykład polimorfizmu funkcji

0: // Listing 5.8 - demonstruje

1: // polimorfizm funkcji

2:

3: #include <iostream>

4:

5: int Double(int);

6: long Double(long);

7: float Double(float);

8: double Double(double);

9:

10: using namespace std;

11:

12: int main()

13: {

14: int myInt = 6500;

15: long myLong = 65000;

16: float myFloat = 6.5F;

17: double myDouble = 6.5e20;

18:

19: int doubledInt;

20: long doubledLong;

21: float doubledFloat;

22: double doubledDouble;

23:

24: cout << "myInt: " << myInt << "\n";

25: cout << "myLong: " << myLong << "\n";

26: cout << "myFloat: " << myFloat << "\n";

27: cout << "myDouble: " << myDouble << "\n";

28:

29: doubledInt = Double(myInt);

30: doubledLong = Double(myLong);

31: doubledFloat = Double(myFloat);

32: doubledDouble = Double(myDouble);

33:

34: cout << "doubledInt: " << doubledInt << "\n";

35: cout << "doubledLong: " << doubledLong << "\n";

36: cout << "doubledFloat: " << doubledFloat << "\n";

37: cout << "doubledDouble: " << doubledDouble << "\n";

38:

39: return 0;

40: }

41:

42: int Double(int original)

43: {

44: cout << "Wewnatrz Double(int)\n";

45: return 2 * original;

46: }

47:

48: long Double(long original)

49: {

50: cout << "Wewnatrz Double(long)\n";

51: return 2 * original;

52: }

53:

54: float Double(float original)

55: {

56: cout << "Wewnatrz Double(float)\n";

57: return 2 * original;

58: }

59:

60: double Double(double original)

61: {

62: cout << "Wewnatrz Double(double)\n";

63: return 2 * original;

64: }

Wynik

myInt: 6500

myLong: 65000

myFloat: 6.5

myDouble: 6.5e+020

Wewnatrz Double(int)

Wewnatrz Double(long)

Wewnatrz Double(float)

Wewnatrz Double(double)

doubledInt: 13000

doubledLong: 130000

doubledFloat: 13

doubledDouble: 1.3e+021

Analiza

Funkcja MyDouble() jest przeciążona dla parametrów typu int, long, float oraz double. Ich prototypy znajdują się w liniach od 5. do 8., zaś definicje w liniach od 42. do 64.

Zwróć uwagę, że w tym przykładzie, w linii 10., użyłem instrukcji using namespace std; poza jakąkolwiek funkcją. Sprawia to, że ta instrukcja stała się dla tego pliku globalną i że przestrzeń nazw std jest używana we wszystkich zdefiniowanych w tym pliku funkcjach[Author ID1: at Mon Oct 22 15:08:00 2001 ].

W ciele głównego programu jest deklarowanych osiem zmiennych lokalnych. W liniach od 14. do 17. są inicjalizowane cztery z tych zmiennych, zaś w liniach od 29. do 32. pozostałym czterem zmiennym są przypisywane wyniki przekazania każdej z pierwszych czterech zmiennych do funkcji MyDouble(). Zauważ, że w chwili wywoływania tej funkcji, funkcja wywołująca nie rozróżnia, która wersja ma zostać wywołana; po prostu przekazuje argument, i to już zapewnia wywołanie właściwej wersji.[Author ID1: at Mon Oct 22 15:09:00 2001 ]a kompilator zajmuje się resztą.[Author ID1: at Mon Oct 22 15:10:00 2001 ]

Kompilator sprawdza argumenty i na tej podstawię wybiera właściwą wersję[Author ID1: at Mon Oct 22 15:10:00 2001 ]z[Author ID1: at Mon Oct 22 15:10:00 2001 ] funkcji MyDouble(). Z wypisywanych komunikatów wynika, że wywoływane są kolejno poszczególne wersje funkcji (tak jak mogliśmy się spodziewać).

Zagadnienia związane z funkcjami

Ponieważ funkcje są istotną częścią programowania, omówimy teraz kilka zagadnień, które mogą się okazać przydatne przy rozwiązywaniu [Author ID1: at Mon Oct 22 15:17:00 2001 ]pewnych problemów.[Author ID1: at Mon Oct 22 15:17:00 2001 ] [Author ID1: at Mon Oct 22 15:18:00 2001 ]cię zainteresować w momencie natrafienia na rzadko występujące problemy.[Author ID1: at Mon Oct 22 15:17:00 2001 ] Właściwe wykorzystanie funkcji typu inline może pomóc w zwiększeniu wydajności programu. Natomiast rekurencyjne wywoływani[Author ID1: at Mon Oct 22 15:19:00 2001 ]e [Author ID1: at Mon Oct 22 15:19:00 2001 ]re[Author ID1: at Mon Oct 22 15:19:00 2001 ]kurencja[Author ID1: at Mon Oct 22 15:19:00 2001 ] funkcji jest jednym z tych cudownych elementów [Author ID1: at Mon Oct 22 15:20:00 2001 ]zagadnień[Author ID1: at Mon Oct 22 15:20:00 2001 ] programowania, które mogą łatwo rozwiązać skomplikowane problemy, trudne do rozwiązania w inny sposób.

Funkcje typu inline

Gdy definiujesz funkcję, kompilator zwykle tworzy w pamięci osobny zestaw instrukcji. Gdy wywołujesz funkcję, wykonanie programu przechodzi (wykonuje skok) [Author ID1: at Mon Oct 22 15:21:00 2001 ]do tego zestawu instrukcji, zaś gdy funkcja skończy działanie, wykonanie wraca do instrukcji następnej po wywołaniu funkcji. Jeśli wywołujesz funkcję dziesięć razy, program za każdym razem „skacze” do tego samego zestawu instrukcji. Oznacza to, że istnieje tylko jedna kopia funkcji, a nie dziesięć.

Z wchodzeniem do [Author ID1: at Mon Oct 22 15:22:00 2001 ]funkcji i wychodzeniem z niej wiąże się pewien niewielki narzut. Okazuje się, że pewne funkcje są bardzo małe, zawierają tylko jedną czy dwie linie kodu, więc istnieje możliwość poprawienia efektywności działania programu przez rezygnację z wykonywania skoków w celu wykonania jednej czy dwóch krótkich instrukcji. Gdy programiści mówią o efektywności, zwykle mają na myśli szybkość działania programu; jeśli unikniemy wywoływania funkcji, program będzie działał szybciej.

Jeśli funkcja zostanie zadeklarowana ze słowem kluczowym inline, kompilator nie tworzy prawdziwej funkcji tylko kopiuje kod z funkcji typu inline bezpośrednio do kodu funkcji wywołującej (w miejscu wywołania funkcji inline). Nie odbywa się żaden skok; program działa tak, jakbyś zamiast wywołania funkcji wpisał instrukcje tej funkcji ręcznie.

Zauważ, że funkcje typu inline mogą oznaczać duże koszty (w sensie czasu procesora). Gdy funkcja jest wywoływana w dziesięciu różnych miejscach programu, jej kod jest kopiowany do każdego z tych dziesięciu miejsc. Niewielkie zwiększenie szybkości może zostać zniwelowane przez znaczny wzrost objętości pliku wykonywalnego, co w efekcie może doprowadzić do spowolnienia działania programu!

Współczesne kompilatory prawie zawsze lepiej radzą sobie z podjęciem takiej decyzji niż programista, dlatego dobrym pomysłem jest rezygnacja z deklarowania funkcji jako inline, chyba że faktycznie składa się ona z jednej czy dwóch linii. Jeśli masz jakiekolwiek wątpliwości, zrezygnuj z użycia słowa kluczowego inline.

Funkcja typu inline została przedstawiona na listingu 5.9.

Listing 5.9. Przykład funkcji typu inline

0: // Listing 5.9 - demonstruje funkcje typu inline

1:

2: #include <iostream>

3:

4: inline int Double(int);

5:

6: int main()

7: {

8: int target;

9: using std::cout;

10: using std::cin;

11: using std::endl;

12:

13: cout << "Wpisz liczbe: ";

14: cin >> target;

15: cout << "\n";

16:

17: target = Double(target);

18: cout << "Wynik: " << target << endl;

19:

20: target = Double(target);

21: cout << "Wynik: " << target << endl;

22:

23:

24: target = Double(target);

25: cout << "Wynik: " << target << endl;

26: return 0;

27: }

28:

29: int Double(int target)

30: {

31: return 2*target;

32: }

Wynik

Wpisz liczbe: 20

Wynik: 40

Wynik: 80

Wynik: 160

Analiza

W linii 4. funkcja MyDouble() jest deklarowana jako funkcja typu inline, otrzymująca parametr typu int i zwracająca wartość całkowitą. Ta deklaracja jest taka sama, jak w przypadku innych prototypów, jednak tuż przed typem zwracanej wartości zastosowano słowo kluczowe inline.

Program kompiluje się do kodu, który ma postać taką, jakby w każdym miejscu wystąpienia instrukcji

target = Double(target);

ręcznie wpisano

target = 2 * target;

W czasie działania programu instrukcje są już na miejscu, wkompilowane do pliku .obj. Dzięki temu unika się „skoków” w wykonaniu kodu (kosztem nieco obszerniejszego pliku wykonywalnego).

UWAGA Słowo kluczowe inline jest wskazówką dla kompilatora, że dana funkcja może być funkcją kopiowaną do kodu. Kompilator może jednak zignorować tę wskazówkę i stworzyć zwyczajną, wywoływaną funkcję.

Rekurencja

Funkcja może wywoływać samą siebie. Nazywa się to rekurencją (lub rekursją). Rekurencja może być bezpośrednia lub pośrednia. Rekurencja bezpośrednia ma miejsce, gdy funkcja wywołuje samą siebie; rekurencja pośrednia następuje wtedy, gdy funkcja wywołuje inną funkcję, która z kolei (być może także pośrednio) wywołuje funkcję pierwotną.

Niektóre problemy najłatwiej rozwiązuje się stosując właśnie rekurencję. Zwykle są to czynności, w trakcie których operuje się na danych, a potem w podobny sposób operuje się na wyniku. Oba rodzaje rekurencji, pośrednia i bezpośrednia, występują w dwóch wersjach: takiej, która się kończy i zwraca wynik, oraz takiej, która się nigdy nie kończy i zwraca błąd czasu działania. Programiści uważają, że ta druga jest bardzo zabawna (gdy przytrafia się komuś innemu).

Należy pamiętać, że gdy funkcja wywołuje siebie samą, tworzona jest nowa kopia lokalnych zmiennych tej funkcji. Zmienne lokalne w wersji wywoływanej są zupełnie niezależne od zmiennych lokalnych w wersji wywołującej i w żaden sposób nie mogą na siebie wpływać. Zmienne lokalne w funkcji main() również nie są związane ze zmiennymi lokalnymi w wywoływanej przez nią funkcji (ilustrował to listing 5.4).

Aby zilustrować zastosowanie rekurencji, wykorzystajmy obliczanie ciągu Fibonacciego:

1, 1, 2, 3, 5, 8, 13, 21, 34...

Każda wartość, począwszy od trzeciej, stanowi sumę dwóch poprzednich elementów. Zadaniem Fibonacciego może być na przykład wyznaczenie dwunastego elementu takiego ciągu.

Aby rozwiązać to zadanie, musimy dokładnie sprawdzać ciąg. Pierwsze dwa elementy mają wartość jeden. Każdy kolejny element stanowi sumę dwóch poprzednich elementów. Np., siódmy element jest sumą elementu piątego i szóstego. Przyjmujemy regułę, że n-ty element jest sumą n-2 i n-1 elementu (przy założeniu, że n jest większe od dwóch).

Funkcje rekurencyjne wymagają istnienia warunku zatrzymania (tzw. warunku stopu). Musi wydarzyć się coś, co powoduje zatrzymanie rekurencji, gdyż w przeciwnym razie nigdy się ona nie skończy (tzn. zakończy się błędem działania programu). W ciągu Fibonacciego warunkiem stopu jest n < 3 (tzn. gdy n stanie się mniejsze od trzech, możemy przestać pracować nad zadaniem).

Algorytm jest to zestaw kroków podejmowanych w celu rozwiązania zadania. Jeden z algorytmów obliczania elementów ciągu Fibonacciego jest następujący:

  1. Poproś użytkownika o podanie numeru elementu ciągu.

  2. Wywołaj funkcję fib(), przekazując jej uzyskany od użytkownika numer elementu.

  3. Funkcja fib() sprawdza argument (n). Jeśli n < 3, zwraca wartość 1; w przeciwnym razie wywołuje (rekurencyjnie) samą siebie, przekazując jako argument wartość n-2. Następnie wywołuje się ponownie, przekazując wartość n-1, po czym zwraca sumę pierwszego i drugiego wywołania.

Gdy wywołasz fib(1), zwróci ona wartość 1. Gdy wywołasz fib(2), także zwróci 1. Jeśli wywołasz [Author ID1: at Mon Oct 22 15:23:00 2001 ]f[Author ID1: at Mon Oct 22 15:23:00 2001 ]ib(3)[Author ID1: at Mon Oct 22 15:23:00 2001 ], to zwróci ona sumę z wywołań [Author ID1: at Mon Oct 22 15:23:00 2001 ]fib(2)[Author ID1: at Mon Oct 22 15:23:00 2001 ] oraz [Author ID1: at Mon Oct 22 15:23:00 2001 ]fib(1)[Author ID1: at Mon Oct 22 15:23:00 2001 ]. Ponieważ [Author ID1: at Mon Oct 22 15:24:00 2001 ]fib(2)[Author ID1: at Mon Oct 22 15:24:00 2001 ] zwraca 1 [Author ID1: at Mon Oct 22 15:24:00 2001 ]a [Author ID1: at Mon Oct 22 15:24:00 2001 ]fib(1)[Author ID1: at Mon Oct 22 15:24:00 2001 ] też[Author ID1: at Mon Oct 22 15:24:00 2001 ] zwr[Author ID1: at Mon Oct 22 15:24:00 2001 ]aca 1, [Author ID1: at Mon Oct 22 15:24:00 2001 ]fib(3)[Author ID1: at Mon Oct 22 15:24:00 2001 ] zwróci 2 ([Author ID1: at Mon Oct 22 15:24:00 2001 ]sumę 1+1). Jeżeli[Author ID1: at Mon Oct 22 15:25:00 2001 ] [Author ID1: at Mon Oct 22 15:23:00 2001 ]wywołasz [Author ID1: at Mon Oct 22 15:25:00 2001 ]fib(4)[Author ID1: at Mon Oct 22 15:25:00 2001 ], to zwróci ona sumę z wywołań [Author ID1: at Mon Oct 22 15:25:00 2001 ]fib(3)[Author ID1: at Mon Oct 22 15:25:00 2001 ] oraz [Author ID1: at Mon Oct 22 15:25:00 2001 ]fib(2)[Author ID1: at Mon Oct 22 15:25:00 2001 ]. [Author ID1: at Mon Oct 22 15:25:00 2001 ]Już wiesz, że [Author ID1: at Mon Oct 22 15:26:00 2001 ]fib(3)[Author ID1: at Mon Oct 22 15:26:00 2001 ] zwraca 2 (z wywołań [Author ID1: at Mon Oct 22 15:26:00 2001 ]fib(2)[Author ID1: at Mon Oct 22 15:26:00 2001 ] i [Author ID1: at Mon Oct 22 15:26:00 2001 ]fib(1)[Author ID1: at Mon Oct 22 15:26:00 2001 ]) oraz, że [Author ID1: at Mon Oct 22 15:26:00 2001 ]fib(2)[Author ID1: at Mon Oct 22 15:26:00 2001 ] zwraca 1. [Author ID1: at Mon Oct 22 15:26:00 2001 ]fib(4)[Author ID1: at Mon Oct 22 15:26:00 2001 ] zsumuje te liczby i zwróci 3[Author ID1: at Mon Oct 22 15:26:00 2001 ] (co stanowi czwarty [Author ID1: at Mon Oct 22 15:26:00 2001 ]element[Author ID1: at Mon Oct 22 15:27:00 2001 ] szeregu[Author ID1: at Mon Oct 22 15:26:00 2001 ]).[Author ID1: at Mon Oct 22 15:26:00 2001 ] [Author ID1: at Mon Oct 22 15:25:00 2001 ]Wiemy więc że [Author ID1: at Mon Oct 22 15:23:00 2001 ]fib(3)[Author ID1: at Mon Oct 22 15:23:00 2001 ] zwraca wartość [Author ID1: at Mon Oct 22 15:23:00 2001 ]2[Author ID1: at Mon Oct 22 15:23:00 2001 ] (w wyniku wywołania [Author ID1: at Mon Oct 22 15:23:00 2001 ]fib(1)[Author ID1: at Mon Oct 22 15:23:00 2001 ] i [Author ID1: at Mon Oct 22 15:23:00 2001 ]fib(2)[Author ID1: at Mon Oct 22 15:23:00 2001 ]) oraz że [Author ID1: at Mon Oct 22 15:23:00 2001 ]fib(2)[Author ID1: at Mon Oct 22 15:23:00 2001 ] zwraca wartość [Author ID1: at Mon Oct 22 15:23:00 2001 ]1[Author ID1: at Mon Oct 22 15:23:00 2001 ], więc[Author ID1: at Mon Oct 22 15:23:00 2001 ] [Author ID1: at Mon Oct 22 15:23:00 2001 ]fib(4)[Author ID1: at Mon Oct 22 15:23:00 2001 ] zsumuje te wartości i zwróci [Author ID1: at Mon Oct 22 15:23:00 2001 ]3[Author ID1: at Mon Oct 22 15:23:00 2001 ], czyli wartość czwartego elementu --> ciągu[Author ID1: at Mon Oct 22 15:23:00 2001 ][Author:PaG] [Author ID1: at Mon Oct 22 15:23:00 2001 ].[Author ID1: at Mon Oct 22 15:23:00 2001 ]

Gdy wywołasz fib(5), zwróci ona sumę fib(4) oraz fib(3). Sprawdziliśmy, że fib(4) zwraca 3, zaś fib(3) zwraca 2, więc zwróconą sumą będzie 5.

Ta metoda nie jest najbardziej efektywnym sposobem rozwiązywania tego problemu (w fib(20) funkcja fib() jest wywoływana 13 529 razy!), ale działa. Bądź ostrożny — jeśli podasz zbyt dużą liczbę, w komputerze zabraknie pamięci potrzebnej do działania programu. Przy każdym wywołaniu funkcji fib() rezerwowany jest fragment pamięci. Gdy funkcja wraca, pamięć jest zwalniana. W przypadku rekurencji pamięć jest wciąż rezerwowana przed zwolnieniem, więc może się bardzo szybko skończyć. Listing 5.10 przedstawia implementację funkcji fib().

OSTRZEŻENIE Gdy uruchomisz listing 5.10, użyj niewielkiej liczby (mniejszej niż 15). Ponieważ program używa rekurencji, może zużyć mnóstwo pamięci.

Listing 5.10. Rekurencyjne obliczanie elementów ciągu Fibonacciego

0: // Obliczanie ciągu Fibonacciego z użyciem rekurencji

1: #include <iostream>

2:

3: int fib (int n);

4:

5: int main()

6: {

7:

8: int n, answer;

9: std::cout << "Podaj numer elementu ciagu: ";

10: std::cin >> n;

11:

12: std::cout << "\n\n";

13:

14: answer = fib(n);

15:

16: std::cout << "Wartoscia " << n << "-go elementu ciagu ";

17: std::cout << "Fibonacciego jest " << answer << "\n";

18: return 0;

19: }

20:

21: int fib (int n)

22: {

23: std::cout << "Przetwarzanie fib(" << n << ")... ";

24:

25: if (n < 3 )

26: {

27: std::cout << "Zwraca 1!\n";

28: return (1);

29: }

30: else

31: {

32: std::cout << "Wywoluje fib(" << n-2 << ") ";

33: std::cout << "oraz fib(" << n-1 << ").\n";

34: return( fib(n-2) + fib(n-1));

35: }

36: }

Wynik

Podaj numer elementu ciagu: 6

Przetwarzanie fib(6)... Wywoluje fib(4) oraz fib(5).

Przetwarzanie fib(4)... Wywoluje fib(2) oraz fib(3).

Przetwarzanie fib(2)... Zwraca 1!

Przetwarzanie fib(3)... Wywoluje fib(1) oraz fib(2).

Przetwarzanie fib(1)... Zwraca 1!

Przetwarzanie fib(2)... Zwraca 1!

Przetwarzanie fib(5)... Wywoluje fib(3) oraz fib(4).

Przetwarzanie fib(3)... Wywoluje fib(1) oraz fib(2).

Przetwarzanie fib(1)... Zwraca 1!

Przetwarzanie fib(2)... Zwraca 1!

Przetwarzanie fib(4)... Wywoluje fib(2) oraz fib(3).

Przetwarzanie fib(2)... Zwraca 1!

Przetwarzanie fib(3)... Wywoluje fib(1) oraz fib(2).

Przetwarzanie fib(1)... Zwraca 1!

Przetwarzanie fib(2)... Zwraca 1!

Wartoscia 6-go elementu ciagu Fibonacciego jest 8

UWAGA Niektóre kompilatory mają problem z użyciem operatorów wewnątrz [Author ID1: at Mon Oct 22 15:29:00 2001 ]w [Author ID1: at Mon Oct 22 15:29:00 2001 ]instrukcji zawierającej [Author ID1: at Mon Oct 22 15:29:00 2001 ]cout. Jeśli w linii 32. pojawi się ostrzeżenie, umieść nawiasy wokół operacji odejmowania tak, by linie 32. i 33. wyglądały następująco:

32: std::cout << "Wywoluje fib(" << (n-2) << ") ";

33: std::cout << "oraz fib(" << (n-1) << ").\n";

Analiza

W linii 9. program prosi o podanie numeru elementu ciągu i przypisuje ten numer zmiennej n. Następnie wywołuje funkcję fib(), przekazując jej tę wartość. Wykonanie przechodzi do funkcji fib(), która wypisuje wartość swojego argumentu w linii 23.

W linii 25. argument n jest sprawdzany w celu upewnienia się czy jest mniejszy od 3; jeśli tak, funkcja fib() zwraca wartość 1. W przeciwnym razie zwraca sumę wartości otrzymanych w wyniku wywołania funkcji fib() z argumentami n-2 oraz n-1.

Funkcja nie może zwrócić tej sumy do momentu powrotu z obu wywołań fib(). Możemy sobie wyobrazić [Author ID1: at Mon Oct 22 15:30:00 2001 ]przedstawić[Author ID1: at Mon Oct 22 15:30:00 2001 ] jak program ciągle wykonuje skoki do [Author ID1: at Mon Oct 22 15:30:00 2001 ]fib[Author ID1: at Mon Oct 22 15:30:00 2001 ]zagłębia się coraz bardziej[Author ID1: at Mon Oct 22 15:30:00 2001 ], do chwili, w której natrafia na wywołanie, w którym funkcja fib() zwraca wartość. Jedyne wywołania zwracające wartość bezpośrednio to wywołania fib(2) oraz fib(1). Te wartości są następnie przekazywane w górę, do oczekujących na nie funkcji, które z kolei przekazują sumę do swoich funkcji wywołujących. Tę rekurencję dla funkcji fib() przedstawiają rysunki 5.4 oraz 5.5.

Rysunek 5.4. Użycie rekurencji

0x01 graphic

Rysunek 5.5. Powrót z rekurencji

0x01 graphic

W tym przykładzie n ma wartość 6, dlatego w funkcji main() jest wywoływana funkcja fib(6). Wykonanie przechodzi do funkcji fib(), w której (w linii 25.) następuje sprawdzenie czy n jest mniejsze od 3. Wartość n jest większa od 3, więc funkcja fib(6) zwraca sumę wartości zwracanych przez funkcje fib(4) oraz fib(5).

34: return( fib(n-2) + fib(n-1));

Oznacza to, że odbywa się wywołanie fib(4) (ponieważ n == 6, więc fib(n-2) to w istocie fib(4)) oraz wywołanie fib(5) (czyli fib(n-1)), po czym funkcja, w której się znajdujemy (w tym przypadku fib(6)) czeka, aż te wywołania zwrócą wartości. Gdy wartości te zostaną zwrócone, funkcja ta zwraca rezultat sumowania tych wartości.

Ponieważ fib(5) otrzymuje argument większy od 3, funkcja fib() zostaje wywołana ponownie, tym razem z argumentami 3 i 4. Funkcja fib(4) wywołuje z kolei funkcje fib(2) oraz fib(3).

Wypisywane komunikaty pokazują te wywołania oraz zwracane wartości. Skompiluj, zbuduj i uruchom ten program, podając wartość 1, następnie 2, potem 3 i tak aż do 6. Uruchamiając program uważnie śledź komunikaty.

To doskonała okazja, aby rozpocząć samodzielne eksperymenty z debuggerem. Umieść punkt przerwania w linii 21., po czym obserwuj[Author ID1: at Mon Oct 22 15:31:00 2001 ] [Author ID1: at Mon Oct 22 15:32:00 2001 ]wchodź wewnątrz ([Author ID1: at Mon Oct 22 15:31:00 2001 ]into[Author ID1: at Mon Oct 22 15:31:00 2001 ])[Author ID1: at Mon Oct 22 15:31:00 2001 ] [Author ID1: at Mon Oct 22 15:32:00 2001 ]każde [Author ID1: at Mon Oct 22 15:32:00 2001 ]go[Author ID1: at Mon Oct 22 15:32:00 2001 ] wywołanie[Author ID1: at Mon Oct 22 15:32:00 2001 ]a[Author ID1: at Mon Oct 22 15:32:00 2001 ] funkcji fib(), śledząc wartość n w każdym rekurencyjnym wywołaniu tej funkcji.

W programach C++ rekurencja nie jest używana zbyt często, ale może stanowić wydajne i eleganckie narzędzie rozwiązywania pewnych problemów.

UWAGA Rekurencja jest elementem programowania zaawansowanego. Została tu zaprezentowana, ponieważ zrozumienie podstaw jej działania może okazać się przydatne, jednak nie przejmuj się zbytnio, jeśli nie zrozumiałeś w pełni wszystkich jej szczegółów.

Jak działają funkcje — rzut oka „pod maskę”

Gdy wywołujesz daną funkcję, program przechodzi do tej funkcji, przekazywane są parametry i następuje wykonanie ciała funkcji. Gdy funkcja zakończy działanie, zwracana jest wartość (chyba, że zwracana jest wartość typu void) i sterowanie powraca do funkcji wywołującej.

Jak to się odbywa? Skąd kod wie, gdzie skoczyć? Gdzie są przechowywane przekazywane zmienne? Co się dzieje ze zmiennymi zadeklarowanymi w ciele funkcji? W jaki sposób jest przekazywana wartość zwracana przez funkcję? Skąd kod wie, w którym miejscu ma wznowić działanie po powrocie z funkcji?

Większość książek wprowadzających w zagadnienia programowania nie próbuje odpowiadać na te pytania, ale bez zrozumienia tych mechanizmów pisanie programów wciąż pozostaje „programowaniem z elementami magii.” Wyjaśnienie zasad działania funkcji wymaga poruszenia tematu pamięci komputera.

Poziomy abstrakcji

Jednym z największych wyzwań dla początkujących programistów jest konieczność posługiwania się wieloma poziomami abstrakcji. Oczywiście, komputery są jedynie urządzeniami elektronicznymi. Nie mają pojęcia o oknach czy menu, nie znają programów ani instrukcji, a nawet nie wiedzą nic o zerach i jedynkach. W rzeczywistości jedyne zmiany, jakie zauważają, to zmiany napięcia mierzonego w odpowiednich punktach układów elektronicznych. Nawet to jest dla nich pewną abstrakcją: w rzeczywistości elektryczność jest tylko wygodną intelektualną koncepcją dla zaprezentowania działania cząstek subatomowych, które z kolei są abstrakcją dla czegoś innego (!).

Bardzo niewielu programistów zadaje sobie trud zejścia poniżej poziomu wartości w pamięci RAM. W końcu nie trzeba znać fizyki cząsteczkowej, aby prowadzić samochód, robić kanapki czy kopać piłkę; nie trzeba też znać się na elektronice, aby programować komputer.

Konieczne jest jednak zrozumienie, w jaki sposób jest zorganizowana pamięć komputera. Bez wyraźnego obrazu tego, gdzie znajdują się tworzone zmienne i w jaki sposób przekazywane są wartości między funkcjami, programowanie nadal pozostanie tajemnicą.

Dzielenie[Author ID1: at Mon Oct 22 15:33:00 2001 ] Podział [Author ID1: at Mon Oct 22 15:33:00 2001 ]pamięci

Gdy uruchamiasz program, system operacyjny (taki jak DOS, Unix czy Microsoft Windows) przygotowuje różne obszary pamięci (w zależności od wymagań kompilatora). Jako programista C++, często będziesz miał do czynienia z globalną przestrzenią nazw, stertą, rejestrami, przestrzenią kodu oraz stosem.

Zmienne globalne występują w globalnej przestrzeni nazw. O globalnej przestrzeni nazw i stercie pomówimy dokładniej w następnych rozdziałach, teraz skupimy się na rejestrach, przestrzeni kodu oraz stosie.

Rejestry są specjalnym obszarem pamięci wbudowanym w procesor (CPU, Central Processing Unit). Odpowiadają za wewnętrzne wykonywanie programu przez procesor. Większość tego, co dzieje się w rejestrach, wykracza poza tematykę tej książki; interesuje nas tylko zestaw rejestrów, który w danej chwili wskazuje następną instrukcję kodu. Zestaw rejestrów nosi wspólną nazwę wskaźnika instrukcji (ang. instruction pointer). Zadaniem wskaźnika instrukcji jest śledzenie, która linia kodu ma zostać wykonana jako następna.

Kod występuje w przestrzeni kodu[Author ID1: at Mon Oct 22 15:33:00 2001 ], która jest częścią pamięci przygotowaną tak, by zawierała binarną postać instrukcji stanowiących [Author ID1: at Mon Oct 22 15:33:00 2001 ]stworzonych jako[Author ID1: at Mon Oct 22 15:33:00 2001 ] program. Każda linia kodu źródłowego została przetłumaczona na serię instrukcji procesora, z których każda znajduje się w pamięci pod określony adresem. Wskaźnik instrukcji zawiera adres następnej instrukcji przeznaczonej przeznaczonej[Author ID1: at Mon Oct 22 15:34:00 2001 ]do wykonania. Ilustruje to rysunek 5.6.

Rys. 5.6. Wskaźnik instrukcji

0x01 graphic

Stos jest specjalnym obszarem pamięci, zaalokowanym przez program w celu przechowywania danych potrzebnych wszystkim funkcjom programu. Jest nazywany stosem, gdyż stanowi kolejkę LIFO (last-in, first-out — ostatni wchodzi, pierwszy wychodzi), przypominającą stos talerzy w restauracji (pokazany na rysunku 5.7).

Rys. 5.7. Stos

0x01 graphic

„Ostatni wchodzi, pierwszy wychodzi” - oznacza, że to, co zostanie umieszczone na stosie jako ostatnie, zostanie z niego zdjęte jako pierwsze. Większość kolejek przypomina kolejki w sklepie: pierwsza osoba w kolejce jest obsługiwana jako pierwsza. Stos przypomina stos monet: gdy ułożysz na stole dziesięć monet, jedna na drugiej, a następnie część z nich zabierasz, zabierasz najpierw te monety, które ułożyłeś jako ostatnie.

Gdy dane są umieszczane (ang. push) na stosie, stos rośnie; gdy są zdejmowane ze stosu (ang. pop), stos maleje. Nie ma możliwości wyjęcia talerza ze stosu bez zdjęcia wszystkich talerzy, które zostały umieszczone na nim później.

Stos talerzy jest najczęściej przedstawianą analogią. Jest ona poprawna, ale działanie pamięci wygląda nieco inaczej. Bardziej odpowiednie jest wyobrażenie sobie szeregu pojemników ułożonych jeden na drugim. Szczytem stosu jest ten pojemnik, na który w danej chwili wskazuje wskaźnik stosu (ang. stack pointer), będący jeszcze jednym rejestrem.

Każdy z pojemników ma kolejny adres, a jeden z tych adresów jest przechowywany w rejestrze wskaźnika stosu. Wszystko, co znajduje się poniżej tego magicznego adresu, znanego jako szczyt stosu, jest uważane za zawartość stosu. Wszystko, co znajduje się powyżej szczytu stosu, jest uważane za znajdujące się poza stosem, a co za tym idzie, niepoprawne. Ilustruje to rysunek 5.8.

Rys. 5.8. Wskaźnik stosu

0x01 graphic

Gdy odkładasz daną na stos, jest ona umieszczana w pojemniku znajdującym się powyżej wskaźnika stosu, a następnie wskaźnik stosu jest przesuwany o jeden pojemnik w górę. Gdy zdejmujesz daną ze stosu, jedyną czynnością odbywającą się w rzeczywistości jest przesunięcie wskaźnika stosu o jeden pojemnik w dół. Pokazuje to rysunek 5.9.

Rys. 5.9. Przesunięcie wskaźnika stosu

0x01 graphic

Dane powyżej wskaźnika stosu (czyli poza stosem) mogą (ale nie muszą) ulec zmianie w dowolnej chwili. Wartości te nazywamy „odpadami” (aby lepiej uświadomić sobie, że nie powinniśmy na nie liczyć).

Stos i funkcje

Poniżej przedstawiono przybliżony opis tego, co się dzieje, gdy program przechodzi do wykonania funkcji. (Poszczególne rozwiązania różnią się, w zależności od systemu operacyjnego i kompilatora).

  1. Zwiększany jest adres we wskaźniku instrukcji i wskazuje on instrukcję następną po tej[Author ID1: at Mon Oct 22 15:34:00 2001 ], która [Author ID1: at Mon Oct 22 15:34:00 2001 ]wywołuje[Author ID1: at Mon Oct 22 15:34:00 2001 ]aniu[Author ID1: at Mon Oct 22 15:34:00 2001 ] funkcję[Author ID1: at Mon Oct 22 15:35:00 2001 ]i[Author ID1: at Mon Oct 22 15:35:00 2001 ]. Ten adres jest następnie umieszczany na stosie; stanowi adres powrotu z funkcji.

  2. Na stosie jest tworzone miejsce dla zadeklarowanego typu wartości zwracanej przez funkcję. Gdy zwracany typ jest zadeklarowany jako int, w przypadku systemu z dwubajtowymi liczbami całkowitymi, na stos są odkładane dwa kolejne bajty, ale nie jest w nich umieszczana żadna wartość („odpady”, które się w nich dotąd znajdowały, pozostają tam nadal).

  3. Do wskaźnika instrukcji jest ładowany adres wywoływanej funkcji (ten adres jest zawarty w kodzie aktualnie wykonywanej instrukcji wywołania funkcji), dzięki czemu następna wykonywana instrukcja będzie już instrukcją funkcji.

  4. Odczytywany jest adres bieżącego szczytu stosu, następnie zostaje on umieszczony w specjalnym wskaźniku nazywanym ramką stosu (ang. stack frame). Wszystko, co zostanie umieszczone na stosie od tego momentu, jest uważane za „lokalne” dla funkcji.

  5. Na stosie umieszczane są argumenty funkcji.

  6. Wykonywana jest instrukcja wskazywana przez wskaźnik instrukcji (następuje wykonanie pierwszej instrukcji w funkcji).

  7. W trakcie ich definiowania, lokalne zmienne zostają umieszczane na stosie.

Gdy funkcja jest gotowa do powrotu, zwracana wartość jest umieszczana w miejscu stosu zarezerwowanym w kroku 2. Następnie stos jest zwijany (tzn. wskaźnik stosu przesuwa się) aż do wskaźnika ramki stosu, co oznacza odrzucenie wszystkich lokalnych zmiennych i argumentów funkcji.

Zwracana wartość jest zdejmowana ze stosu i przypisywana jako wartość instrukcji wywołania funkcji. Następnie ze stosu zdejmowana jest wartość odłożona w kroku 1.; wartość ta zostaje umieszczona we wskaźniku instrukcji. Program, posiadając wartość zwróconą przez funkcję, wznawia działanie od instrukcji następującej bezpośrednio po instrukcji wywołania funkcji.

Niektóre ze szczegółów tego procesu zmieniają się w zależności od kompilatora i komputera, ale podstawowy jego przebieg jest niezmienny. Gdy wywołujesz funkcję, na stosie odkładany jest adres powrotu i argumenty. W trakcie działania tych funkcji, na stos są odkładane zmienne lokalne. Gdy funkcja wraca, ze stosu zostaje usunięte wszystko.

W następnych rozdziałach poznamy inne miejsca pamięci, używane do przechowywania danych, które muszą istnieć dłużej niż czas życia funkcji.

2 Część I Podstawy obsługi systemu WhizBang (Nagłówek strony)

2 F:\korekta\r05-06.doc[Author ID2: at Wed Nov 14 08:26:00 2001 ]C:\Moje dokumenty\jr\doc\Korekt_rzeczo\Kopia r05-05.doc[Author ID2: at Wed Nov 14 08:26:00 2001 ]

Poniżej brak jednego akapitu - str. 118.



Wyszukiwarka

Podobne podstrony:
C++1 1, r01-06, Szablon dla tlumaczy
C++1 1, r07-06, Szablon dla tlumaczy
C++1 1, r03-06, Szablon dla tlumaczy
C++1 1, r09-06, Szablon dla tlumaczy
C++1 1, r02-06, Szablon dla tlumaczy
Ksiazki c++, rdodC-06, Szablon dla tlumaczy
Linux Programming Professional, r-13-01, Szablon dla tlumaczy
C++1 1, r00-05, Szablon dla tlumaczy
Praktyczne programowanie, R 5c-04, Szablon dla tlumaczy
Dreamweaver 4 Dla Każdego, ROZDZ07, Szablon dla tlumaczy
Dreamweaver 4 Dla Każdego, ROZDZ03, Szablon dla tlumaczy
Praktyczne programowanie, R 6-04, Szablon dla tlumaczy
Doc20, Szablon dla tlumaczy
Doc04, Szablon dla tlumaczy
Doc17, Szablon dla tlumaczy
Dreamweaver 4 Dla Każdego, STR 788, Szablon dla tlumaczy
Doc19, Szablon dla tlumaczy
C, Szablon dla tlumaczy

więcej podobnych podstron