LEKCJA 36: FUNKCJE WIRTUALNE i KLASY ABSTRAKCYJNE. ________________________________________________________________ W trakcie tej lekcji dowiesz się, co mawia żona programisty, gdy nie chce być obiektem klasy abstrakcyjnej. ________________________________________________________________ FUNKCJE W PEŁNI WIRTUALNE (PURE VIRTUAL). W skrajnych przypadkach wolno nam umieścić funkcję wirtualną w klasie bazowej nie definiując jej wcale. W klasie bazowej umieszczamy wtedy tylko deklarację-prototyp funkcji. W następnych pokoleniach klas pochodnych mamy wtedy pełną swobodę i możemy zdefiniować funkcję wirtualną w dowolny sposób - adekwatny dla potrzeb danej klasy pochodnej. Możemy np. do klasy bazowej (ang. generic class) dodać prototyp funkcji wirtualnej funkcja_eksperymentalna() nie definiując jej w (ani wobec) klasie bazowej. Sens umieszczenia takiej funkcji w klasie bazowej polege na uzyskaniu pewności, iż wszystkie klasy pochodne odziedziczą funkcję funkcja_eksperymentalna(), ale każda z klas pochodnych wyposaży tę funkcję we własną definicję. Takie postępowanie może okazać się szczególnie uzasadnione przy tworzeniu biblioteki klas (class library) przeznaczonej dla innych użytkowników. C++ w wersji instalacyjnej posiada już kilka gotowych bibliotek klas. Funkcje wirtuale, które nie zostają zdefiniowane - nie posiadają zatem ciała funkcji - nazywane są funkcjami w pełni wirtualnymi (ang. pure virtual function). O KLASACH ABSTRAKCYJNYCH. Jeśli zadeklarujemy funkcję CZwierzak::Oddychaj() jako funkcję w pełni wirtualną, oprócz słowa kluczowego virtual, trzeba tę informację w jakiś sposób przekazać kompilatorowi C++. Aby C++ wiedział, że naszą intencją jest funkcja w pełni wirtalna, nie możemy zadeklarować jej tak: class CZwierzak { ... public: virtual void Oddychaj(); ... }; a następnie pominąć definicję (ciało) funkcji. Takie postępowanie C++ uznałby za błąd, a funkcję - za zwykłą funkcję wirtualną, tyle, że "niedorobioną" przez programistę. Naszą intencję musimy zaznaczyć już w definicji klasy w taki sposób: class CZwierzak { ... public: virtual void Oddychaj() = 0; ... }; Informacją dla kompilatora, że chodzi nam o funkcję w pełni wirtualną, jest dodanie po prototypie funkcji "= 0". Definiując klasę pochodną możemy rozbudować funkcję wirtualną np.: class CZwierzak { ... public: virtual void Oddychaj() = 0; ... }; class CPiesek : public CZwierzak { ... public: void Oddychaj() { cout << "Oddycham..."; } ... }; Przykładem takiej funkcji jest funkcja Mów() z przedstawionego poniżej programu. Zostawiamy ją w pełni wirtualną, ponieważ różne obiekty klasy CZLOWIEK i klas pochodnych class CZLOWIEK { public: void Jedz(void); virtual void Mow(void) = 0; //funkcja WIRTUALNA }; class NIEMOWLE : public CZLOWIEK { public: void Mow(void); // Tym razem BEZ slowa virtual }; /* Tu definiujemy metodę wirtualną: -------------------- */ void NIEMOWLE::Mow(void) { cout << "Nie Umiem Mowic! \n"; }; mogą mówić na różne sposoby... Obiekt Niemowle, dla przykładu, nie chce mówić wcale, ale z innymi obiektami może być inaczej. Wyobraź sobie np. obiekt klasy Żona (żona to przecież też człowiek !). class Zona : public CZLOWIEK { public: void Mow(void); } W tym pokoleniu definicja wirtualnej metody Mow() mogłaby wyglądać np. tak: void Zona::Mow(void) { cout << "JA NIE MAM CO NA SIEBIE WLOZYC !!! "; cout << "DLACZEGO KOWALSKI ZARABIA ZAWSZE WIECEJ NIZ TY ?!!!"; //... itd., itd., itd... } [P128.CPP] #include "iostream.h" class CZLOWIEK { public: void Jedz(void); virtual void Mow(void) = 0; }; void CZLOWIEK::Jedz(void) { cout << "MNIAM, MNIAM..."; }; class Zona : public CZLOWIEK { public: void Mow(void); //Zona mowi swoje }; //bez wzgledu na argumenty (typ void) void Zona::Mow(void) { cout << "JA NIE MAM CO NA SIEBIE WLOZYC !!!"; cout << "DLACZEGO KOWALSKI ZARABIA ZAWSZE WIECEJ NIZ TY ?!!!"; } class NIEMOWLE : public CZLOWIEK { public: void Mow(void); }; void NIEMOWLE::Mow(void) { cout << "Nie Umiem Mowic! \n"; }; main() { NIEMOWLE Dziecko; Zona Moja_Zona; Dziecko.Jedz(); Dziecko.Mow(); Moja_Zona.Mow() return 0; } Przykładowa klasa CZŁOWIEK jest klasą ABSTRAKCYJNĄ. Jeśli spróbujesz dodać do powyższego programu np.: CZLOWIEK Facet; Facet.Jedz(); uzyskasz komunikat o błędzie: Cannot create a variable for abstract class "CZLOWIEK" (Nie mogę utworzyć zmiennych dla klasy abstrakcyjnej "CZLOWIEK" [???] KLASY ABSTRAKCYJNE. ________________________________________________________________ * Po klasach abstrakcyjnych MOŻNA dziedziczyć! * Obiektów klas abstrakcyjnych NIE MOŻNA stosować bezpośrednio! ________________________________________________________________ Ponieważ wyjaśniliśmy, dlaczego klasy są nowymi typami danych, więc logika (i sens) innej rozpowszechnionej nazwy klas abstrakcyjnych - ADT - Abstract Data Type (Abstrakcyjne Typy Danych) jest chyba zrozumiała i oczywista. ZAGNIEŻDŻANIE KLAS I OBIEKTÓW. Może się np. zdarzyć, że klasa stanie się wewnętrznym elementem (ang. member) innej klasy i odpowiednio - obiekt - elementem innego obiektu. Nazywa się to fachowo "zagnieżdżaniem" (ang. nesting). Jeśli, dla przykładu klasa CB będzie zawierać obiekt klasy CA: class CA { int liczba; public: CA() { liczba = 0; } //Konstruktor domyslny CA(int x) { liczba = x; } void operator=(int n) { liczba = n } }; class CB { CA obiekt; public: CB() { obiekt = 1; } }; Nasze klasy wyposażyliśmy w konstruktory i od razu poddaliśmy overloadingowi operator przypisania = . Aby prześledzić kolejność wywoływania funkcji i sposób przekazywania parametrów pomiędzy tak powiązanymi obiektami rozbudujemy każdą funkcję o zgłoszenie na ekranie. class CA { int liczba; public: CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; } CA(int x) { liczba = x; cout << "->CA(int) "; } void operator=(int n) { liczba = n; cout << "->operator "; } }; class CB { CA obiekt; public: CB() { obiekt = 1; cout << "->Konstruktor CB() "; } }; Możemy teraz sprawdzić, co stanie się w programie po zadeklarowaniu obiektu klasy CB: [P129.CPP] # include "iostream.h" class CA { int liczba; public: CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; } CA(int x) { liczba = x; cout << "->CA(int) "; } void operator=(int n) { liczba = n; cout << "->operator "; } }; class CB { CA obiekt; public: CB() { obiekt = 1; cout << "->Konstruktor CB() "; } }; main() { CB Obiekt; return 0; } Po uruchomieniu programu możesz przekonać się, że kolejność działań będzie następująca: C:\>program -> CA(), CA_O::liczba = 0 ->operator ->Konstruktor CB() Skoro oprócz zainicjowania obiektu klasy pochodnej nie robimy w programie dokładnie nic, nie dziwmy się ostrzeżeniu Warning: Obiekt is never used... Jest to sytuacja trochę podobna do komunikacji pomiędzy konstruktorami klas bazowych i pochodnych. Jeśli zaprojektujemy prostą strukturę klas: class CBazowa { private: int liczba; public: CBazowa() { liczba = 0} CBazowa(int n) { liczba = n; } }; class CPochodna : public CBazowa { public: CPochodna() { liczba = 0; } CPochodna(int x) { liczba = x; } }; problem przekazywania parametrów między konstruktorami klas możemy w C++ rozstrzygnąć i tak: class CPochodna : public CBazowa { public: CPochodna() : CBazowa(0) { liczba = 0; } CPochodna(int x) { liczba = x; } }; Będzie to w praktyce oznaczać wywołanie konstruktora klasy bazowej z przekazanym mu argumentem 0. Podobnie możemy postąpić w stosunku do klas zagnieżdżonych: [P130.CPP] #include "iostream.h" class CA { int liczba; public: CA() { liczba = 0; cout << "-> CA(), CA_O::liczba = 0 "; } CA(int x) { liczba = x; cout << "->CA(int) "; } void operator=(int n) { liczba = n; cout << "->operator "; } }; class CB { CA obiekt; public: CB() : CA(1) {} }; main() { CB Obiekt; return 0; } Eksperymentując z dwoma powyższymi programami możesz przekonać się, jak przebiega przekazywanie parametrów pomiędzy konstruktorami i obiektami klas bazowych i pochodnych. JESZCZE RAZ O WSKAŹNIKU *this. Szczególnie ważnym wskaźnikiem przy tworzeniu klas pochodnych i funkcji operatorowych może okazać się pointer *this. Oto przykład listy. [P131.CPP] # include "string.h" # include "iostream.h" class CLista { private: char *poz_listy; CLista *poprzednia; public: CLista(char*); CLista* Poprzednia() { return (poprzednia); }; void Pokazuj() { cout << '\n' << poz_listy; } void Dodaj(CLista&); ~CLista() { delete poz_listy; } }; CLista::CLista(char *s) { poz_listy = new char[strlen(s)+1]; strcpy(poz_listy, s); poprzednia = NULL; } void CLista::Dodaj(CLista& obiekt) { obiekt.poprzednia = this; } main() { CLista *ostatni = NULL; cout << '\n' << "Wpisanie kropki [.]+[Enter] = Quit \n"; for(;;) { cout << "\n Wpisz nazwe (bez spacji): "; char TAB[70]; cin >> TAB; if (strncmp(TAB, ".", 1) == 0) break; CLista *lista = new CLista(TAB); if (ostatni != NULL) ostatni->Dodaj(*lista); ostatni = lista; } for(; ostatni != NULL;) { ostatni->Pokazuj(); CLista *temp = ostatni; ostatni = ostatni->Poprzednia(); delete (temp); } return 0; } Z reguły to kompilator nadaje wartość wskaźnikowi this i to on automatycznie dba o przyporządkowanie pamięci obiektom. Pointer this jest zwykle inicjowany w trakcie działania konstruktora obiektu.