Krótki kurs C++ Część III(1).doc

(62 KB) Pobierz

Krótki kurs C++ Część III

1. Funkcje

1.1. Wstęp

Programy komputerowe są tworami bardzo złożonymi. Umieszczenie wszystkich instrukcji w funkcji main() byłoby niewygodne z kilku powodów: po pierwsze wykrycie ewentualnych błędów byłoby utrudnione, po drugie instrukcje, które powtarzałyby się trzeba byłoby wpisywać ponownie. Funkcje pozwalają programiście na budowanie programu z mniejszych części oraz ponowne wykorzystywanie tych komponentów. Programista może pisać funkcje do wykonywania określonych zadań. Mogą one być wykonywane w wielu miejscach w programie. Nic nie stoi na przeszkodzie, aby funkcje były wykonywane w różnych programach. Funkcje są wywoływane. Wywołanie funkcji określa jej nazwę oraz argumenty przekazywane do funkcji.

1.2. Prototypy funkcji

Programy z poprzednich części kursu zawierały wywołania do funkcji z biblioteki standardowej. Teraz nauczymy się pisania własnych funkcji, które będą mogły być wykorzystywane w naszych programach. Zanim wywołamy funkcję musimy przekazać kompilatorowi informację o nazwie funkcji, argumentach przez nią pobieranych oraz o zwracanej wartości. Kompilator używa tej informacji do sprawdzania wywołań funkcji tzn. czy argumenty jakie przekazujemy do funkcji są odpowiednie itp. . Pierwsze wersje kompilatorów nie przeprowadzały tego typu sprawdzenia, co prowadziło do częstych błędów.

Mały przykład:

#include <iostream.h> //prototyp funkcji max int max(int,int,int ); int main(){ int x, y, z; cout<<"Wprowadz trzy liczby calkowite:"<<endl; cin>>x>>y>>z; cout<<"Najwieksza z tych liczb to:"<<max(x,y,z)<<endl; return 0; } //definicja funkcji max int max(int a,int b,int c){ int max=a; if(b>max) max=b; if(c>max) max=c; return max; }

Powyższy program pobiera trzy liczby całkowite od użytkownika i określa która z nich jest największa. Linia:

//prototyp funkcji max int max(int,int,int );

zawiera prototyp funkcji max. Pierwszy wyraz z lewej - int - określa typ wartości zwracany przez funkcję, w tym wypadku mamy liczbę całkowitą. Kolejny wyraz jest identyfikatorem (nazwą) funkcji. Wyrazy umieszczone w nawiasach określają typ argumentów pobieranych przez funkcję - są to trzy liczby całkowite. Prototyp mógłby również wyglądać tak:

//prototyp funkcji max int max(int a, int b, int c );

przy czym identyfikatory argumentów w prototypach są pomijane przez kompilator. Można umieszczać je tam dla celów dokumentacyjnych. Kompilator odwołuje się do prototypu funkcji, aby sprawdzić czy wywołanie funkcji max zawiera poprawny zwracany typ, poprawną liczbę argumentów, poprawne ich typy i porządek. Prototyp funkcji nie jest wymagany jeżeli funkcja pojawiła się przed pierwszym wywołaniem tzn. jeżeli ciało funkcji max() umieścilibyśmy przed main() prototyp funkcji max() można pominąć. Część prototypu funkcji, która zawiera nazwę funkcji i typy jej argumentów nazywana jest sygnaturą. Gdybyśmy prototyp funkcji określili jako :

void max(int, int, int );

kompilator wygenerowałby błąd ponieważ zawracany typ void w prototypie nie zgadzałby się z tym w nagłówku. Ważną cechą prototypów jest koercja argumentów. Polega to na tym, że wartości argumentów, które nie odpowiadają dokładnie typom parametrów w prototypie funkcji są przekształcane do właściwego typu zanim funkcja zostanie wywołana. Przekształcenia czasami mogą prowadzić do błędów jeżeli reguły promocji nie zostaną zachowane. Reguły te określają jak typy danych mogą zostać przekształcone do innych typów bez utraty danych. Na przykład gdybyśmy do funkcji max() przekazali double jako argument ułamkowa część double byłaby obcięta. Reguły promocji stosuje się też do wyrażeń miesznych czyli takich, które zwierają dwa lub więcej typów danych. Oto tabela hierarchii promocji:

long double double float unsigned long long unsigned int int unsigned short short unsigned short short char

Przekształcanie z wyższego typu danych w hierarchii do niższego może prowadzić do błędów.

1.3. Definicja funkcji.

Funkcja max jest wywoływana w funkcji main w linii :

cout<<"Najwieksza z tych liczb to:"<<max(x,y,z)<<endl;

Funkcja ta otrzymuje kopie wartości x,y,z w parametrach a, b, c. Następnie porównuje ona ze sobą argumenty i zwraca największy z nich. Wynik jest przesyłany do main(), gdzie funkcja ta była wywołana. Definicja funkcji max() jest w llinii:

//definicja funkcji max int max(int a,int b,int c){

wyraz int najbardziej z lewej strony oznacza, że funkcja zwraca wynik całkowity (integer). Po nim następuje identyfikator oznaczający nazwę funkcji, w nawiasie podane są parametry jakich oczekuje funkcja oraz ich nazwy. Definicja funkcji ma więc postać :

typ_zwracanej_wartości nazwa_funkcji (lista_parametrów) { ciało funkcji; }

typ_zwracanej_wartości jest typem danych zwracanych przez funkcję. Typ void oznacza, że funkcja nie zwraca wartości. Nieokreślony typ jest przyjmowany przez kompilator jako int. nazwa_funkcji jest dowolnym dozwolonym identyfikatorem. Lista_parametrów jest to lista oddzielonych przecinkami deklaracji parametrów otrzymywanych przez funkcję. Jeśli funkcja nie otrzymuje żadnych argumentów lista_parametrów jest typu void lub jest pusta np.:

void max(void) void max()

Typ parametru musi być wyraźnie napisany przed każdym parametrem. Po nawiasach () nie umieszczamy średnika.

Są trzy sposoby na opuszczenie ciała funkcji:

·         jeśli funkcja nie zwraca wartości, sterowanie jest zwracane wtedy, gdy osiągnięty zostanie prawy nawias kończący funkcję,

·         jeśli funkcja nie zwraca wartości, sterowanie jest zwracane przez wykonanie wyrażenia return;,

·         jeśli funkcja zwraca wartość to wyrażenie return wyrażenie;.

1.3.1. Argumenty domyślne

Programista może określić domyślną wartość argumentu. Kiedy argument jest pominięty w wywołaniu, jego wartość jest wstawiana przez kompilator i przekazywana w wywołaniu. Argumenty te powinny być położone jak najbardziej z prawej strony. Argumenty te powinny być określone wraz z pierwszym wystąpieniem nazwy funkcji, z reguły będzie to prototyp.

#include <iostream.h> //prototyp fuknji poleProstokata int poleProstokata( int=1,int=1 ); int main(int argc, char** argv){ int x, y; cout<<"Wprowadz dwie liczby calkowite:"<<endl; cin>>x>>y; cout<<"Pole prostokata wynosi:"<<poleProstokata(x,y)<<endl; cout<<"Pole prostokata z argumentami domyslnymi :" <<poleProstokata()<<'\n'<<"Pole prostokata z jednym argumentem domyslnym:"<<poleProstokata(x)<<endl; return 0; } //definicja funkcji poleProstokata int poleProstokata(int a,int b){ return a*b; }

Klasy pamięci

Każdy identyfikator ma atrybuty obejmujące : klasę pamięci, zasięg i połączenia. Do określenia klasy pamięci w C++ służą specyfikatory klas pamięci. C++ zawiera cztery specyfikatory klas pamięci : auto, register, extern, static. Klasa pamięci identyfikatora określa okres w którym identyfikator znajduje się w pamięci. Niektóre identyfikatory istnieją bardzo krótko, a inne przez cały czas wykonywania programu. Zasięg identyfikatora określa z jakiego miejsca w programie można się odwołać do identyfikatora. Do niektórych identyfikatorów można się odwołać z każdego miejsca w programie ( np. zmienne globalne ), a do innych tylko z niektórych części. Połączenia identyfikatorów określają czy w programach złożonych z wielu plików źródłowych identyfikator jest znany tylko w bieżącym pliku źródłowym, czy w każdym.

Specyfikatory klas pamięci mogą być podzielone na dwie klasy :

·         statyczną,

·         automatyczną.

Tylko zmienne mogą być elementami automatycznych klas pamięci. Zmienne lokalne i parametry funkcji są zwykle elementami automatycznych klas pamięci. Specyfikator auto wyraźnie deklaruje zmienne automatycznej klasy pamięci. Zmienne tego typu są tworzone podczas wykonywania bloku, w którym są deklarowane i są niszczone, gdy następuje wyjście z niego. Zmienne lokalne są domyślnie automatycznej klasy pamięci, więc słowo kluczowe auto jest rzadko używane. Specyfikator klasy pamięci register może być umieszczony przed deklaracją zmiennej automatycznej. Specyfikator ten oznacza, że kompilator powinien umieszczać tą zmienną raczej w rejestrze procesora. Kompilator może ignorować deklaracje register.

Słowa kluczowe extern i static są używane do deklarowania zmiennych i funkcji statycznej klasy pamięci. Zmienne takie istnieją od momentu, w którym program rozpoczyna wykonywanie. Pamięć jest im przydzielana i inicjowana tylko raz. Globalne zmienne i funkcje mają domyślnie klasę pamięci extern. Są one tworzone poprzez umieszczenie ich poza jakąkolwiek funkcją. Do zmiennych tych i funkcji może się odwoływać każda funkcja w pliku. Zmienne używane tylko w określonej funkcji powinny być deklarowane jako zmienne lokalne. Deklarowanie zmiennych jako globalne utrudnia wykrywanie błędów. Zmienne lokalne zadeklarowane za słowa static są nadal znane tylko w funkcji w której zostały zadeklarowane, inaczej od zmiennych automatycznych zachowują swoje wartości po wyjściu z funkcji. Następnym razem przy wywołaniu funkcji zmienne te mają taką wartość jaką miały przy wyjściu z funkcji.

#include <iostream.h> //prototyp fukncji zmienneStatyczne int zmienneStatyczne( ); int main(){ for(int i=0;i<10;i++) cout<<i<<". wywolanie funkcji zmienneStatyczne()" <<zmienneStatyczne()<<'\n'; return 0; } //definicja funkcji zmienneStatyczne int zmienneStatyczne( ){ static int a=0; a++; return a; }

Reguły zasięgu

Reguły zasięgu określają z jakiego miejsca w pliku można się odwołać do danego identyfikatora. Pięcioma zasięgami dla identyfikatora są zasięg funkcji, zasięg pliku, zasięg bloku, zasięg prototypu funkcji, zasięg prototypu funkcji oraz zasięg klasy.

Identyfikator zadeklarowany poza jakąkolwiek funkcją ma zasięg pliku. Można do niego odwoływać się od miejsca w którym został on zadeklarowany aż do końca pliku.

Etykiety są identyfikatorami mającymi zasięg funkcji. Mogą one być używane gdziekolwiek w funkcji, w której się pojawiają, ale nie można się do nich odwołać spoza ciała funkcji. Etykiety są używane w poleceniach switch i goto.

Identyfikatory zadeklarowane wewnątrz bloku mają zasięg bloku. Zasięg bloku rozpoczyna się w miejscu deklaracji identyfikatora i kończy się po osiągnięciu nawiasu klamrowego - } - . Zasięg bloku mają zmienne lokalne zdefiniowane na początku funkcji, a także parametry funkcji.

Jedynymi identyfikatorami o zasięgu prototypu funkcji są identyfikatory użyte na liście jej parametrów.

#include <iostream.h> //zmienna globalna int a=1; void funkcja(); int main(){ //zmienna lokalna w main int a=5; cout<<"Zmienna lokalna w main:"<<a<<'\n'; //nowy blok { int a=10; cout<<"Zmienna lokalna w bloku:"<<a<<'\n'; } funkcja(); cout<<"Zmienna lokalna w main:"<<a<<'\n'; return 0; } //definicja funkcji void funkcja( ){ cout<<"Zmienna globalna to:"<<a<<'\n'; return; }

Rekurencja

Funkcja rekurencyjna jest to funkcja, która wywołuje bezpośrednio sama siebie lub pośrednio przez inną funkcję. Funkcja rekurencyjna jest wywoływana do rozwiązania określonego problemu. Funkcja ta z reguły wie jak rozwiązać najprostszy przypadek. Jeśli wywoływana jest do problemu złożonego dzieli go na dwa elementy: ten, który potrafi rozwiązać i ten, którego nie potrafi rozwiązać. Ten drugi element jest nieznacznie upraszczany. Funkcja wywołuje swoją kopię do pracy z tym uproszczonym elementem. Nazywane jest to krokiem rekurencji. Gdy problem zostaje uproszczony do przypadku podstawowego wywołania rekurencyjne kończą się i funkcja zwraca wartość. Typowym problemem dla rekurencji może być silnia oraz szereg Fibonacciego. Silnia nieujemnej liczby całkowitej pisana jest n! (wymawiana jako "n silnia"). Jest to iloczyn:

n*(n-1)*(n-2)*...*1

przy czym 1! jest równe 1 i 0! jest równe 0. Dla przykładu: 3!=3*2*1. Rekurencyjna silnia ma postać:

n!=n*(n-1)!

np.:

3!=3*2*1 3!=3*(2!)

Oto program obliczający silnię z liczby podanej przez użytkownika:

#include <iostream.h> //prototyp funkcji long silniaRekurencyjnie(long); int main(){ long a; cout<<"Wprowadz liczbe calkowita \n"; cin>>a; cout<<"Silnia liczby"<<a<<" wynosi:"<<silniaRekurencyjnie(a)<<endl; return 0; } //definicja funkcji long silniaRekurencyjnie(long liczba){ //przypadek podstawowy if(liczba<=1) return 1; else //przypadek złożony upraszczamy nieznacznie i funkcja //wywołuje samą siebie return liczba*silniaRekurencyjnie(liczba-1); }

Funkcja silniaRekurencyjnie() została zadeklarowana jako funkcja oczekująca parametru typu long oraz zwracająca wynik typu long. Proponuję Wam abyście zmodyfikowali tak ten program, aby było widać jakie argumenty funkcja otrzymuje oraz jaką wartość zwraca.

Szereg Fibonacciego rozpoczyna się od 0 i 1 i charakteryzuje się tym, że kolejna liczba jest sumą dwóch poprzednich. Stosunek kolejnych liczb Fibonacciego jest równy w przybliżeniu 1.618. Liczba ta nazywana jest złotym podziałem. Szereg Fibonacciego może być przedstawiony rekurencyjnie :

fibonacci(0)=0 fibonacci(1)=1 fibonacci(n)=fibonacci(n-1)+fibonacci(n-2)

Oto program obliczający szereg Fibonacciego rekurencyjnie :

#include <iostream.h> //prototyp funkcji long fibonacci(long); int main(){ long a; cout<<"Wprowadz liczbe calkowita \n"; cin>>a; cout<<"fibonacci ("<<a<<") wynosi:"<<fibonacci(a)<<endl; return 0; } //definicja funkcji long fibonacci(long liczba){ //przypadek podstawowy if(liczba<=0) return 0; if(liczba==1) return 1; else //przypadek złożony dzielimy na dwie części return fibonacci(liczba-1)+fibonacci(liczba-2); }

Rekurencja ma wiele wad. Obliczenie 20 liczby Fibonacciego wymagać będzie 2^20 wywołań (ok. miliona wywołań !). Będzie to oczywiście znacznie obciążać zasoby komputera.

Referencje i parametry referencji

Funkcję można wywołać na dwa sposoby: wywołanie przez wartość (ang. call by value) oraz wywołanie przez referencje. Do tej pory funkcje wywoływaliśmy poprzez wartość. Gdy wywołujemy funkcję poprzez wartość tworzona jest kopia argumentu, który przekazujemy w wywołaniu. Kopia ta jest przekazywana do funkcji. Wszystkie działania w ciele funkcji są wykonywane na tej kopii i nie oddziaływują na oryginalną wartość zmiennej. Dodatnią stroną tego typu wywołania jest to, że zapobiega to ujemnym skutkom ubocznym. Ujemną stroną jest to, że w przypadku dużych struktur danych kopiowanie zbiera dużo czasu i pamięci.

Dzięki wywołaniu przez referencje funkcja wywołująca przekazuje funkcji wywoływanej zdolność do bezpośredniego dostępu do danych. Nie jest tworzona kopia, a wszelkie modyfikacje są dokonywane na zmiennej bezpośrednio. Jeśli parametr ma być przekazywany do funkcji przez referencje po typie parametru a przed identyfikatorem umieść znak ampersandu (&). Oto program demonstrujący różnicę między tymi dwoma wywołaniami :

#include <iostream.h> //prototypy funkcji int wywolaniePrzezWartosc (int); void wywolaniePrzezReferencje(int &); int main(){ int a; cout<<"Wprowadz liczbe calkowita \n"; cin>>a; cout<<"a przed wywołaniem funkcji przez wartość:"<<a<<'\n'; cout<<"Wartość zwracana przez funkcję wywołaniePrzezWartość:" <<wywolaniePrzezWartosc(a)<<'\n'; cout<<"a po wywołaniu funkcji przez wartość:"<<a<<'\n'; cout<<"a przed wywołaniem funkcji przez referencje :"<<a<<'\n'; cout<<"Wywołanie funkcji przez referencje\n"; wywolaniePrzezReferencje(a); cout<<"a po wywołaniu funkcji przez referencje :"<<a<<endl; return 0; } //definicja funkcji int wywolaniePrzezWartosc(int liczba){ liczba=liczba*5; return liczba; } void wywolaniePrzezReferencje(int &liczba1){ liczba1=liczba1*5; }

Przeciążanie funkcji

W C++ można definiować kilka funkcji o tej samej nazwie, przy czym muszą się one różnić parametrami. Jest to nazywane przeciążanem funkcji. Na przykład : mamy funkcję obliczającą pole powierzchni prostokąta. Możemy zdefiniować kilka funkcji polePowierzchni. Będą one pobierały jako argumenty różne typy (np. jedna int, druga double itp.). Kiedy funkcja przeciążona jest wywoływana kompilator C++ wybiera właściwą poprzez sprawdzenie liczby typów i porządku argumentów w tym wywołaniu. Przeciążanie funkcji jest stosowane tam, gdzie przeprowadzane są takie same obliczenia na różnych typach danych. Oto podany przykład:

#include <iostream.h> //prototypy funkcji int polePowierzchni(int bokX,int bokY); double polePowierzchni(double bokX,double bokY); int main(){ int a=5,b=4; double c=5.8,d=3.2; cout<<"Pole powierzchni z argumentami całkowitymi " <<polePowierzchni(a,b)<<'\n'; cout<<"Pole powierzchni z argumentami rzeczywistymi " <<polePowierzchni(c,d)<<endl; return 0; } //definicja funkcji int polePowierzchni(int bokX,int bokY){ return bokX*bokY; } double polePowierzchni(double bokX,double bokY){ return bokX*bokY; }

Ćwiczenia

1.      Jaki jest zasięg zmiennej x w main.

2.      Napisz funkcję określającą dla pary liczb całkowitych, czy pierwsza jest wielokrotnością drugiej.

3.      Napisz funkcję min zwracającą najmniejszą z trzech liczb całkowitych.

4.      Liczba jest liczbą pierwszą jeżeli dzieli się tylko przez 1 i przez samą siebie. Napisz funkcję, która określi czy dana liczba jest liczbą pierwszą.

5.      Czy main() może być wywołana rekurencyjnie ? Sprawdź to.

6.      Największy wspólny dzielnik x i y jest największą liczbą całkowitą przez którą x i y dzielą się bez reszty. Napisz funkcję , która będzie pobierała dwa argumenty i zwracała NWD.

 

...
Zgłoś jeśli naruszono regulamin