C Plus Plus
Advertisement


Funkcje

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.

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>
using namespace std;
int x, y, z;
//prototyp i 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;
}
int main()
{
  cout<<"Wprowadz trzy liczby calkowite :"<<endl;
  cin>>x>>y>>z;
  cout<<"Najwieksza z tych liczb to :"<<max(x,y,z)<<endl;

return 0;
}

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

do błędów.

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 była wywołana. Definicja funkcji max() jest w liniach:

 //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;
}

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. W C++ typ wartości zwracanej musi być zawsze określony(nawet jeśli funkcja nic nie zwraca wówczas musimy zasygnalizować to kompilatorowi słowem kluczowym void). 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:

  1. jeśli funkcja nie zwraca wartości, sterowanie jest zwracane wtedy, gdy osiągnięty zostanie prawy nawias kończący funkcję,
  2. jeśli funkcja nie zwraca wartości, sterowanie jest zwracane przez wykonanie wyrażenia return;,
  3. jeśli funkcja zwraca wartość to wyrażenie return wyrażenie;.

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 domyślne powinny być określone wraz z pierwszym wystąpieniem nazwy funkcji, z reguły będzie to prototyp.

#include <iostream>

using namespace std;
int x, y;
template <typename T>
//definicja funkcji poleProstokata
T poleProstokata(T a=1,T b=1)
{
 T wynik = a*b;
 return wynik;
}
int main()
{
 cout<<"Wprowadz dwie liczby calkowite :"<<endl;
 cin>>x>>y;

 cout<<"Pole prostokata wynosi :"<<poleProstokata(x,y)<<endl;
 cout<<"Pole prostokata z argumentami domyslnymi :"<<poleProstokata()<<endl;
   <<"Pole prostokata z jednym argumentem domyslnym:"<<poleProstokata(x)<<endl;

 return 0;
}

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:

  1. statyczną,
  2. 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>;
using namespace std;
short int i=0;
//definicja funkcji zmienneStatyczne
short int zmienneStatyczne( )
{
 static short int a=0;
 a++;
 return a;
} 
int main()
{
 for(;i<10;i++)
 cout<<i<<". wywolanie funkcji zmienneStatyczne() "<<zmienneStatyczne()<<endl;
 return 0;
}

Przed funkcją main deklarujemy zmienną i typu short int.

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 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>
using namespace std;

//zmienna globalna
short int a=1;
//definicja funkcji
void funkcja( )
{
 cout<<"Zmienna globalna to :"<<a<<endl;
}
int main()
{
 //zmienna lokalna w main
 short int a=5;
 cout<<"Zmienna lokalna w main :"<<a<<endl;
 //nowy blok
 {
  short int a=10;
  cout<<"Zmienna lokalna w bloku :"<<a<<endl;
 }
 funkcja();
 cout<<"Zmienna lokalna w main :"<<a<<endl;
 return 0;
}

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 1. Dla przykładu 3!=3*2*1. Rekurencyjna silnia ma postać:

n!=n*(n-1)!

np.:

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

Oto program obliczający silnię z trzech liczb podanych przez użytkownika:


#include <iostream>
using namespace std;
short int a;
int b;
long c;
template <typename T>
//definicja funkcji
T silniaRekurencyjnie(T liczba)
{
  return (liczba<=1?1:liczba*silniaRekurencyjnie(liczba-1));
} 

int main()
{
 cout<<"Wprowadź trzy liczby całkowite:"<<endl;
 cin>>a>>b>>c;
 cout<<"Silnia liczby "<<a<<" wynosi :"<<silniaRekurencyjnie(a)<<endl;
 return 0;
}

Funkcja silniaRekurencyjnie() została zadeklarowana jako funkcja oczekująca parametru typu T oraz zwracająca wynik typu T. Wyjaśnienia wymaga tutaj linia:

template <typename T>

Deklaruje ona typ T. 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>
using namespace std;
long a;
//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);
}
int main()
{
 cout<<"Wprowadz liczbe calkowita"<<endl;
 cin>>a;
 cout<<"fibonacci ("<<a<<") wynosi :"<<fibonacci(a)<<endl;
 return 0;
}

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 w efekcie może to doprowadzić do przepełnienia stosu. Dlatego rekurencje należy stosować wówczas gdy liczba wywołań nie będzie zbyt wielka. Rekurencja ma też zalety, podstawową zaletą jest niejako "naturalny" sposób rozwiązywania problemów. W przypadku, gdy złożoność obliczeniowa naszego rekurencyjnego algorytmu jest zbyt duża należy upraszczać rekurencję do wyrażenia nie będącego rekurencyjnym.

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 zabiera 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 referencję po typie parametru a przed identyfikatorem umieść znak ampersandu (&). Oto program demonstrujący różnicę między tymi dwoma wywołaniami:

#include <iostream>
using namespace std;
int a;
template <typename T>
//definicja funkcji
T wywolaniePrzezWartosc(T liczba)
{
 liczba*=5;
 return liczba;
}

void wywolaniePrzezReferencje(T &liczba1)
{
 liczba1*=5;
}
int main()
{
 cout<<"Wprowadz liczbe calkowita"<<endl;
 cin>>a;
 cout<<"a przed wywołaniem funkcji przez wartość :"<<a<<endl;
 cout<<"Wartość zwracana przez funkcję wywolaniePrzezWartosc:"<<wywolaniePrzezWartosc(a)<<endl;
 cout<<"a po wywołaniu funkcji przez wartość :"<<a<<endl;
 cout<<"a przed wywołaniem funkcji przez referencje :"<<a<<endl;
 cout<<"Wywołanie funkcji wywolaniePrzezReferencje"<<endl;
 wywolaniePrzezReferencje(a);
 cout<<"a po wywołaniu funkcji wywolaniePrzezReferencje:"<<a<<endl;
 return 0;
}

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ążaniem 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>
using namespace std;
int a=5,b=4;
double c=5.8,d=3.2;
//definicja funkcji
long polePowierzchni(long bokX,long bokY)
{
 return bokX*bokY;
}
double polePowierzchni(double bokX,double bokY)
{
 return bokX*bokY;
}
int main() 
{
 cout<<"Pole powierzchni z argumentami całkowitymi "<<polePowierzchni(a,b)<<endl;
 cout<<"Pole powierzchni z argumentami rzeczywistymi "<<polePowierzchni(c,d)<<endl;
 return 0;
}

Ale o wiele lepiej jest czasem użyć szablonu. Przykład powyższy mógłby też mieć postać:

#include <iostream>
using namespace std;
int a=5,b=4;
double c=5.8,d=3.2;
template <typename T>
//definicja funkcji
T polePowierzchni(T bokX,T bokY)
{
 return bokX*bokY;
}
int main() 
{
 cout<<"Pole powierzchni z argumentami całkowitymi "<<polePowierzchni(a,b)<<endl;
 cout<<"Pole powierzchni z argumentami rzeczywistymi "<<polePowierzchni(c,d)<<endl;
 return 0;
}

Tym razem, nie tworzyliśmy za funkcją poleProstokata funkcji o tej samej nazwie, lecz przed nią zadeklarowaliśmy szablon funkcji o nazwie T, i parametry i typ zwracanej wartości to tym razem T.

Szablony.

Szablony są podobne do funkcji, używają klas, i są bardzo wygodne w wywołaniu:

Nazwa_Szablonu <parametry> Nazwa_Obiektu;

Tworzymy je tak:

template <parametry>
class Nazwa_Szablonu {
   public:
     Nazwa_Szablonu() {
       //tutaj piszesz zawartość szablonu
     }
}

Ć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.
Advertisement