LEKCJA 35: O ZASTOSOWANIU DZIEDZICZENIA. ________________________________________________________________ Z tej lekcji dowiesz si�, do czego w praktyce programowania szczeg�lnie przydaje si� dziedziczenie. ________________________________________________________________ Dzi�ki dziedziczeniu programista mo�e w pe�ni wykorzysta� gotowe biblioteki klas, tworz�c w�asne klasy i obiekty, jako klasy pochodne wazgl�dem "fabrycznych" klas bazowych. Je�li bazowy zestw danych i funkcji nie jest adekwatny do potrzeb, mo�na np. przes�oni�, rozbudowa�, b�d� przebudowa� bazow� metod� dzi�ki elastyczno�ci C++. Zdecydowana wi�kszo�� standardowych klas bazowych wyposa�ana jest w konstruktory. Tworz�c klas� pochodn� powinni�my pami�ta� o istnieniu konstruktor�w i rozumie� sposoby przekazywania argument�w obowi�zuj�ce konstruktory w przypadku bardziej z�o�onej struktury klas bazowych-pochodnych. PRZEKAZANIE PARAMETR�W DO WIELU KONSTRUKTOR�W. Klasy bazowe mog� by� wyposa�one w kilka wersji konstruktora. Dop�ki nie przeka�emy konstruktorowi klasy bazowej �adnych argument�w - zostanie wywo�any (domy�lny) pusty konstruktor i klasa bazowa b�dzie utworzona z parametrami domy�lnymi. Nie zawsze jest to dla nas najwygodniejsza sytuacja. Je�eli wszystkie, b�d� cho�by niekt�re z parametr�w, kt�re przekazujemy konstruktorowi obiektu klasy pochodnej powinny zosta� przekazane tak�e konstruktorowi (konstruktorom) klas bazowych, powinni�my wyt�umaczy� to C++. Z tego te� powodu, je�li konstruktor jakiej� klasy ma jeden, b�d� wi�cej parametr�w, to wszystkie klasy pochodne wzgl�dem tej klasy bazowej musz� posiada� konstruktory. Dla przyk�adu dodajmy konstruktor do naszej klasy pochodnej Cpochodna: class CBazowa1 { public: CBazowa1(...); //Konstruktor }; class CBazowa2 { public: CBazowa2(...); //Konstruktor }; class Cpochodna : public CBazowa1, CBazowa2 //Lista klas { public: Cpochodna(...); //Konstruktor }; main() { Cpochodna Obiekt(...); //Wywolanie konstruktora ... W momencie wywo�ania kostruktora obiektu klasy pochodnej Cpochodna() przekazujemy kostruktorowi argumenty. Mo�emy (je�li chcemy, nie koniecznie) przekaza� te argumenty konstruktorom "wcze�niejszym" - konstruktorom klas bazowych. Ta mo�liwo�� okazuje si� bardzo przydatna (niezb�dna) w �rodowisku obiektowym - np. OWL i TVL. Oto prosty przyk�ad definiowania konstruktora w przypadku dziedziczenia. Rola konstruktor�w b�dzie polega� na trywialnej operacji przekazania pojedynczego znaku. class CBazowa1 { public: CBazowa1(char znak) { cout << znak; } }; class CBazowa2 { public: CBazowa2(char znak) { cout << znak; } }; class Cpochodna : public CBazowa1, CBazowa2 { public: Cpochodna(char c1, char c2, char c3); }; Cpochodna::Cpochodna(char c1,char c2,char c3) : CBazowa1(c2), CBazowa2(c3) { cout << c1; } Konstruktor klasy pochodnej pobiera trzy argumenty i dwa z nich: c2 --> przekazuje do konstruktora klasy CBazowa1 c3 --> przekazuje do konstruktora klasy CBazowa2 Spos�b zapisu w C++ wygl�da tak: Cpochodna::Cpochodna(char c1,char c2,char c3) : CBazowa1(c2), CBazowa2(c3) Mo�emy zatem przekaza� parametry "w ty�" do konstruktor�w klas bazowych w taki spos�b: kl_pochodna::kl_pochodna(lista):baza1(lista), baza2(lista), ... gdzie: lista - oznacza list� parametr�w odpowiedniego konstruktora. W takiej sytuacji na li�cie argument�w konstruktor�w klas bazowych mog� znajdowa� si� tak�e wyra�enia, przy za�o�eniu, �e elementy tych wyra�e� s� widoczne i dost�pne (np. globalne sta�e, globalne zmienne, dynamicznie inicjowane zmienne globalne itp.). Konstruktory b�d� wykonywane w kolejno�ci: CBazowa1 --> CBazowa2 --> Cpochodna Dzi�ki tym mechanizmom mo�emy �atwo przekazywa� argumenty "wstecz" od konstruktor�w klas pochodnych do konstruktor�w klas bazowych. FUNKCJE WIRTUALNE. Dzia�anie funkcji wirtualnych przypomina rozbudow� funkcji dzi�ki mechanizmowi overloadingu. Je�li, zdefiniowali�my w klasie bazowej funkcj� wirtualn�, to w klasie pochodnej mo�emy definicj� tej funkcji zast�pi� now� definicj�. Przekonajmy si� o tym na przyk�adzie. Zacznijmy od zadeklarowania funkcji wirtualnej (przy pomocy s�owa kluczowego virtual) w klasie bazowej. Zadeklarujemy jako funkcj� wirtualn� funkcj� oddychaj() w klasie CZwierzak: class CZwierzak { public: void Jedz(); virtual void Oddychaj(); }; Wyobra�my sobie, �e chcemy zdefiniowa� klas� pochodn� CRybka Rybki nie oddychaj� w taki sam spos�b, jak inne obiekty klasy CZwierzak. Funkcj� Oddychaj() trzeba zatem b�dzie napisa� w dwu r�nych wariantach. Obiekt Ciapek mo�e t� funkcj� odziedziczy� bez zmian i sapa� spokojnie, z Sardynk� gorzej: class CZwierzak { public: void Jedz(); virtual void Oddychaj() { cout << "Sapie..."; } }; class CPiesek : public CZwierzak { char imie[30]; } Ciapek; class CRybka char imie[30]; public: void Oddychaj() { cout << "Nie moge sapac..."; } } Sardynka; Zwr�� uwag�, �e w klasie pochodnej w deklaracji funkcji s�owo kluczowe virtual ju� nie wyst�puje. W klasie pochodnej funkcja CRybka::Oddychaj() robi wi�cej ni� w przypadku "zwyk�ego" overloadingu funkcji. Funkcja CZwierzak::Oddychaj() zostaje "przes�oni�ta" (ang. overwrite), mimo, �e ilo�� i typ argument�w. pozostaje bez zmian. Taki proces - bardziej drastyczny, ni� overloading nazywany jest przes�anianiem lub nadpisywaniem funkcji (ang. function overriding). W programie przyk�adowym Ciapek b�dzie oddycha� a Sardynka nie. [P127.CPP] # include <iostream.h> class CZwierzak { public: void Jedz(); virtual void Oddychaj() {cout << "\nSapie...";} }; class CPiesek : public CZwierzak { char imie[30]; } Ciapek; class CRybka char imie[30]; public: void Oddychaj() {cout << "\nSardynka: A ja nie oddycham.";} } Sardynka; void main() { Ciapek.Oddychaj(); Sardynka.Oddychaj(); } Funkcja CZwierzak::Oddychaj() zosta�a w obiekcie Sardynka przes�oni�ta przez funkcj� CRybka::Oddychaj() - nowsz� wersj� funkcji-metody pochodz�c� z klasy pochodnej. Overloading funkcji zasadza� si� na "typologicznym pedanty�mie" C++ i na dodatkowych informacjach, kt�re C++ do��cza przy kompilacji do funkcji, a kt�re dotycz� licznby i typ�w argument�w danej wersji funkcji. W przypadku funkcji wirtualnych jest inaczej. Aby wykona� przes�anianie kolejnych wersji funkcji wirtualnej w taki spos�b, funkcja we wszystkich "pokoleniach" musi mie� taki sam prototyp, tj. pobiera� tak� sam� liczb� parametr�w tych samych typ�w oraz zwraca� warto�� tego samego typu. Je�li tak si� nie stanie, C++ potraktuje r�ne prototypy tej samej funkcji w kolejnych pokoleniach zgodnie z zasadami overloadingu funkcji. Zwr��my tu uwag�, �e w przypadku funkcji wirtualnych o wyborze wersji funkcji decyduje to, wobec kt�rego obiektu (kt�rej klasy) funkcja zosta�a wywo�ana. Je�li wywo�amy funkcj� dla obiektu Ciapek, C++ wybierze wersj� CZwierzak::Oddychaj(), natomiast wobec obiektu Sardynka zostanie zastosowana wersja CRybka::Oddychaj(). W C++ wska�nik do klasy bazowej mo�e tak�e wskazywa� na klasy pochodne, wi�c zastosowanie funkcji wirtualnych mo�e da� pewne ciekawe efekty "uboczne". Je�li zadeklarujemy wska�nik *p do obiekt�w klasy bazowej CZwierzak *p; a nast�pnie zastosujemy ten sam wska�nik do wskazania na obiekt klasy pochodnej: p = &Ciapek; p->Oddychaj(); ... p = &Sardynka; p->Oddychaj(); zarz�damy w taki spos�b od C++ rozpoznania w�a�ciwej wersji wirtualnej metody Oddychaj() i jej wywo�ania we w�a�ciwym momencie. C++ mo�e rozpozna�, kt�r� wersj� funkcji nale�a�oby zastosowa� tylko na podstawie typu obiektu, wobec kt�rego funkcja zosta�a wywo�ana. I tu pojawia si� pewien problem. Kompilator wykonuj�c kompilcj� programu nie wie, co b�dzie wskazywa� pointer. Ustawienie pointera na konkretny adres nast�pi dopiero w czasie wykonania programu (run-time). Kompilator "wie" zatem tylko tyle: p->Oddychaj()(); //kt�ra wersja Oddychaj() ??? Aby mie� pewno��, co w tym momencie b�dzie wskazywa� pointer, kompilator musia�by wiedzie� w jaki spos�b b�dzie przebiega� wykonanie programu. Takie wyra�enie mo�e zosta� wykonane "w ruchu programu" dwojako: raz, gdy pointer b�dzie wskazywa� Ciapka (inaczej), a drugi raz - Sardynk� (inaczej): CZwierzak *p; ... for(p = &Ciapek, int i = 0; i < 2; i++) { p->Oddychaj(); p = &Sardynka; } lub inaczej: if(p == &Ciapek) CZwierzak::Oddychaj(); else CRybka::Oddychaj(); Taki efekt nazywa si� polimorfizmem uruchomieniowym (ang. run-time polymorphism). Overloading funkcji i operator�w daje efekt tzw. polimorfizmu kompilacji (ang. compile-time), to funkcje wirtualne daj� efekt polimorfizmu uruchomieniowego (run-time). Poniewa� wszystkie wersje funkcji wirtualnej maj� taki sam prototyp, nie ma innej metody stwierdzenia, kt�r� wersj� funkcji nale�y zastosowa�. Wyb�r w�a�ciwej wersji funkcji mo�e by� dokonany tylko na podstawie typu obiektu, do kt�rego nale�y wersja funkcji-metody. R�nica pomi�dzy polimorfizmem przejawiaj�cym si� na etapie kompilacji i poliformizmem przejawiaj�cym si� na etapie uruchomienia programu jest nazywana r�wnie� wszesnym albo p�nym polimorfizmem (ang. early/late binding). W przy...
ZAZZY