LEKCJA 29: FUNKCJE I OVERLOADING. ________________________________________________________________ W trakcie tej lekcji dowiesz się, jak jeszcze w C++ można wykorzystywać funkcje. ________________________________________________________________ w C++ jedna funkcja może być definiowana wielokrotnie a każda z wersji funkcji może być przystosowana do obsługi innego typu argumentów. C++ wybiera tę właściwą wersję funkcji automatycznie. JEDNA NAZWA FUNKCJI - WIELE ZASTOSOWAŃ. Overloading funkcji bywa czasem w podręcznikach dzielony na odrębne zagadnienia: * funkcja może tolerować różną liczbę argumentów (co dało się spokojnie realizować również w klasycznym C - vide printf()); * funkcja może tolerować różne typy argumentów; * funkcja może realizować różne operacje dla różnych Wyobraźmy sobie, że mamy funkcję wydrukuj(), która potrafi wysłać na ekran otrzymany znak: void wydrukuj(char znak) { cout << znak; } Tak zdefiniowaną funkcję możemy wywołać w programie w następujący sposób: wydrukuj('Z'); Czasem jednak wygodniej byłoby, gdyby nasza funkcja była bardziej elastyczna i pozwalała na wykonanie szerszego zakresu operacji, np.: wydrukuj('Z'); wydrukuj(75); // 75 to kod ASCII znaku, zamiast znaku bezpośr. wydrukuj("Wiecej niz znak - tekst"); W klasycznym języku C wymaga to zdefiniowania nowej funkcji, natomiast w C++ to, że funkcja wydrukuj() została już zdefiniowana w niczym nie przeszkadza. Poniżej definjujemy taką funkcję. ... class KLASA { public: void wydrukuj(char znak); void wydrukuj(int kod_ASCII); void wydrukuj(char *string); //wskaźnik do lancucha } Łańcuch znaków jest widziany jako jednowymiarowa tablica zawierająca dane typu znakowego, czyli w taki sposób: char TABLICA[9] ={ "123456789" }; Definice powinny mieć następującą postać: void KLASA::wydrukuj(char znak) {cout << znak;}; void KLASA::wydrukuj(int kodASCII) {cout << (char) kodASCII;}; void KLASA::wydrukuj(char *string) {cout << string;}; Zapis: cout << (char) kodASCII; oznacza forsowanie typu - zamień typ int na typ char - przyporządkowanie kodowi ASCII - znaku. Wywołanie tej funkcji w programie może spowodować różne działanie, w zależności od typu i ilości argumentów, z którym(i) funkcja zostaje wywołana. Wywołania funkcji mogą wyglądać np. tak: KLASA Obiekt1, Obiekt2; main() { ... Obiekt1.wydrukuj('A'); //Wydrukuje się litera A Obiekt1.wydrukuj(99); //Wydrukuje się litera c Obiekt2.wydrukuj("napis"); //Wydrukuje się napis. ... } Taki sposób postępowania umożliwia funkcjom większą elastyczność i pozwala operować bez konfliktów na różnych rodzajach danych. Język C posiada funkcje służące do kopiowania łańcuchów znakowych: strcpy() i strncpy(). Funkcja biblioteczna strncpy() przerywa proces kopiowania po zakończeniu łańcucha żródłowego, bądź po skopiowaniu zadanej ilości znaków. Dzięki mechanizmowi overloadingu możemy utworzyć naszą własną funkcję kopiuj_string(), która zależnie od sytuacji zadziała jak strcpy(), bądź tak jak strncpy(). [P104.CPP] # include /* dwa porototypy - dwie wersje funkcji kopiuj_string() */ /* source: destination: len: */ void kopiuj_string(char*, const char*); //Dwa argumenty void kopiuj_string(char*, const char*, int); //a tu trzy static char Piggie[20], Kermit[32]; main() { kopiuj_string(Piggie, "Panna Piggie"); kopiuj_string(Kermit, "Kermit - to protokul transmisji", 6); cout << Kermit << " oraz " << Piggie; return 0; } void kopiuj_string(char *destin, const char *source) { while((*destin++ = *source++) != '\0') /* instr. pusta */ ; } void kopiuj_string(char *destin, const char *source, int len) { while (len && (*destin++ = *source++) != '\0') --len; while (len--) *destin++ = '\0'; } [S] Source- Destination. ________________________________________________________________ source - tu: źródłowy łańcuch znaków. Ogólnie - źródło. Typowy skrót src. destin - tu: łańcuch przeznaczenia. Ogólnie destination - miejsce przeznaczenia. Typowy skrót dest, dst, destin. len - tu: długość. ________________________________________________________________ O FUNKCJACH WPLECIONYCH - TYPU inline. Czsami zależy nam na przyspieszeniu działania programu obiektowego (zwykle kosztem zwiększenia długości pliku). Jeśli w źródłowym tekście programu następuje wywołanie funkcji typu inline, to kompilator wstawia w to miejsce całe ciało funkcji (funkcje typu inline nie mają bezpośredniego ani wyłącznego odniesienia do obiektowego stylu programowania). Dla przykładu, jeśli nadalibyśmy naszej funkcji wydrukuj() status funkcji inline, to fragment programu: obiekt.wydrukuj(65); //Kod ASCII zostałby zastąpiony wstawionym w to miejsce ciałem funkcji wydrukuj(): .... cout << (char) 65; .... Jest to skuteczna metoda przyspieszenia działania programów. Jeśli chcemy zastosować technikę funkcji inline w stosunku do metod należących do danej klasy, powinniśmy użyć słowa kluczowego "inline" w definicjach funkcji. Zwróć uwgę, że w samej definicji klasy słowo inline NIE POJAWIA SIĘ: [P105.CPP] # include class Klasa { public: void wydrukuj(char* tekst); void wydrukuj(char Znak); void wydrukuj(int KodASCII); }; inline void Klasa::wydrukuj(char* tekst) { cout << tekst; } inline void Klasa::wydrukuj(char Znak) { cout << Znak; } inline void Klasa::wydrukuj(int KodASCII) { cout << (char) KodASCII; } void main() { Klasa Obiekt; cout << "Obiekt wyprowadza dane: " << '\n'; Obiekt.wydrukuj(65); Obiekt.wydrukuj('B'); Obiekt.wydrukuj("C i juz"); } Wszystkie wersje funkcji wydrukuj() otrzymały status inline. Oznacza to, że funkcje te nie będą w programie wywoływane lecz całe ciała funkcji zostaną wstawione do programu w miejsca wywołań. Jest to mechanizm podobny do wstawiania do programu makrorozkazów z tą różnicą, że w przypadku funkcji inline C++ przeprowadza dodatkowo sprawdzenie zgodności typów argumentów (ang. type checking). W naszym przypadku kompilator C++ wstawi do programu ciało funkcji tyle razy, ile razy funkcja powinna zostać wywoływana. Zastosowanie funkcji inline jest opłacalne, jeżeli ciało funkcji jest stosunkowo krótkie. [!!!] A CZY NIE MOŻNA WEWNĄTRZ KLASY ? ________________________________________________________________ Można. Jeśli umieścimy pełną definicję funkcji wewnątrz definicji klasy, to taka funkcja staje się AUTOMATYCZNIE funkcją typu inline. ________________________________________________________________ Status inline możemy nadać wszystkim trzem wersjom funkcji wydrukuj() umieszczając definicje funkcji bezpośrednio wewnątrz definicji klasy: class Klasa { public: inline void wydrukuj(char* a) { cout << a; } inline void wydrukuj(char z) { cout << z; } inline void wydrukuj(int kod) { cout << (char) kod; } }; W większości przypadków daje to efekt pozytywny. Jeśli definiujemy funkcje wewnątrz klasy, są to zwykle funkcje o krótkim ciele. OVERLOADING KONSTRUKTORÓW. W C++ możemy poddać overloadingowi także konstruktory. UWAGA: destruktorów nie można poddać overloadingowi. Overloading konstruktorów nie wyróżnia się niczym specjalnym. Wyobraźmy sobie, że tworzymy obiekt klasy Klasa o nazwie Obiekt. Jeśli chcemy, by konstruktor przy zakładaniu Obiektu przekazał mu łańcuch znaków "zzzz", możemy to zrobić na dwa sposoby. Raz polecimy konstruktorowi przekazać do obiektu łańcuch znaków "zzzz", a za drugim razem polecimy przekazać do obiektu czterokrotnie znak 'z': Obiekt("zzzz"); /* albo */ Obiekt('z', 4); Jeśli w programie zadeklarujemy obiekt danej klasy, spowoduje to automatyczne wywołanie konstruktora z parametrem podanym w momencie deklaracji obiektu. class Klasa { public: Klasa(char*); Klasa(char, int); }; Wersje konstruktora Klasa::Klasa() powinniśmy zdefiniować tak: Klasa::Klasa(char *tekst) { cout << tekst; } Klasa::Klasa(char Znak, ile = 4); { for(int i = 1; i < ile; i++) cout << Znak; } Dodajmy jeszcze jeden kontruktor domyślny. Konstruktory domyślne działają według zasady, którą w naturalnym języku dałoby się przekazać mniej więcej tak: "dopóki nie zdecydowano inaczej...". Dopóki nie zdecydowano inaczej - obiekt otrzyma znak 'x'. class Klasa { public: Klasa(); Klasa(char*); Klasa(char, int); }; ... Klasa::Klasa(void) { cout << 'x'; } Praktyczne zastosowanie w programie będzie wyglądać tak: [P106.CPP] # include class Klasa { public: Klasa(); Klasa(char*); Klasa(char, int); }; Klasa::Klasa(void) { cout << 'x'; } Klasa::Klasa(char *tekst) { cout << tekst; } Klasa::Klasa(char Znak, int ile = 4) { for(int i = 0; i < ile; i++) cout << Znak; } static char *p = "\nJestem Obiekt."; void main() { Klasa Obiekt1; //Konstr. domyślny Klasa Obiekt2('A'); // ile - domyslnie == 4 Klasa Obiekt3('B', 3); Klasa Obiekt4(p); }