LEKCJA 28: DZIEDZICZENIE ZŁOŻONE. ________________________________________________________________ W trakcie tej lekcji dowiesz się, jak można odziedziczyć wiele cech po wielu różnych przodkach. ________________________________________________________________ Jeśli zechcemy dziedziczyć dalej według schematu dziadek-ojciec-syn-wnuk...? Nic nie stoi na przeszkodzie. Przy okazji zwróć uwagę, że następne pokolenia są coraz bardziej złożone (tak być nie musi, ale może). W przykładzie poniżej dziedziczymy według schematu Punkt-Okrąg-Elipsa. [P100.CPP] //Przyklad dziedziczenia "wielopokoleniowego" #include "stdio.h" #include "conio.h" struct punkt //BAZOWY typ struktur - punkt(x, y) { int x; //wspolrzedne punktu na ekranie int y; }; struct kolo: punkt //Str. pochodna - kolo(x, y, R) { int promien; //wspolrzedne srodka x,y dziedziczymy }; struct elipsa: kolo //dziedziczymy x,y i promien { int mniejszy_promien; //Str. pochodna elipsa(x, y, R, r) }; punkt P; //deklarujemy trzy struktury kolo C; elipsa E; main() { clrscr(); P.x = C.x = E.x = 1; //Nadajemy wartosci polom struktur P.y = C.y = E.y = 2; C.promien = E.promien = 4; E.mniejszy_promien = 3; //Sprawdzamy zawartosc pol struktur printf("%d %d %d %d %d %d \n", P.x, C.x, E.x, P.y, C.y, E.y); printf("%d %d %d", C.promien, E.promien, E.mniejszy_promien ); getch(); return 0; } Można dziedziczyć po więcej niż jednym przodku także w inny sposób. Kwadrat, dla przykładu, dziedziczy cechy po prostokątach i po rombach jednocześnie (jest jednocześnie szczególnym przypadkiem prostokąta i szczególnym przypadkiem rombu). Typ pochodny w tym wypadku, zamiast "dziadka" i "ojca" powinien mieć DWU RÓŻNYCH OJCÓW (!). W C++ takie dziedziczenie po dwu różnych typach bazowych jednocześnie nazywa się DZIEDZICZENIEM WIELOBAZOWYM (ang. multi-base inheritance). A oto przykład takiego dziedziczenia. [P101.CPP] #include struct BAZOWA1 { //Struktura bazowa pierwsza public: void Funkcja_a(void); }; struct BAZOWA2 { //Struktura bazowa druga public: void Funkcja_b(void); }; struct POCHODNA : BAZOWA1, BAZOWA2 //Lista "przodkow" { public: void Funkcja_c(void); }; void BAZOWA1::Funkcja_a(void){cout << "To ja F_a().\n";} void BAZOWA2::Funkcja_b(void){cout << "To ja F_b().\n";} void POCHODNA::Funkcja_c(void){cout << "To ja F_c().\n";} void main() { POCHODNA dziecko; //Dekl. strukt. typu pochodnego dziecko.Funkcja_a(); dziecko.Funkcja_b(); dziecko.Funkcja_c(); } Słowo public jest w strukturach zbędne. Zostało użyte wyłącznie z pobudek "dydaktycznych" - dla zwrócenia uwagi na status funkcji - członków struktury. Zarówno pokoleń w schemacie dziadek-ojciec-syn, jak i struktur (klas) bazowych w schemacie baza_1-baza_2-....-baza_n może być więcej niż 2. DZIEDZICZENIE KLAS. Oto "klasowo-obiektowa" wersja poprzedniego programu przykładowego ze słonikiem Cholerykiem. Typy struktur Zwierzak i Slon nazwiemy klasami, (odpowiednio - klasą bazową i klasą pochodną) a strukturę Slon Choleryk nazwiemy obiektem. [P102.CPP] #include class Zwierzak //Klasa bazowa (base class) { public: int nogi; void jedz(); void spij(); void oddychaj(); }; void Zwierzak::jedz(void) { cout << "Jem conieco...\n"; } void Zwierzak::spij(void) { cout << "Cosik mi sie sni...\n"; } void Zwierzak::oddychaj(void) { cout << "Dysze ciezko...\n"; } class Slon : public Zwierzak { public: int flaga_ssak; void trabi(); void tupie(); }; void Slon::trabi(void) { cout << "Tra-ta-ta...\n"; } void Slon::tupie(void) { cout << "Kroczem...na wschod\n"; } void main() { Slon Obiekt; /* obiekt Obiekt klasy Slon */ Obiekt.nogi = 4; Obiekt.flaga_ssak = 1; cout << "\nNogi odziedziczylem: " << Obiekt.nogi; cout << "\nA teraz kolejno funkcje: \n"; Obiekt.jedz(); Obiekt.spij(); Obiekt.oddychaj(); Obiekt.trabi(); Obiekt.tupie(); if(Obiekt.flaga_ssak) cout << "Jestem ssakiem !"; } Pamiętając o problemie domyślnego statusu członków struktur/public i klas/private) możemy przejść do klas i obiektów. O KLASACH SZCZEGÓŁOWO. Aby wykazać możliwość modularyzacji programu zaprojektujemy moduł w postaci pliku nagłówkowego. Moduł będzie zawierać definicję naszej prywatnej klasy obiektów ZNAK. Zaczynamy od danych, które będą nam potrzebne do tworzenia w programach (różnych !) obiektów typu Znak. class ZNAK { char znak_dany; //Kod ASCII znaku ... Aby obiekt został zainicjowany (tzn. wiedział jakim znakiem ma być w danym programie) dodamy do definicji klasy jednoparametrowy konstruktor class ZNAK { char znak_dany; public: ZNAK(...); ... Dane mogą być prywatne, natomiast konstruktor i funkcje-metody powinny być publiczne, by można było wywoływać je w programach. Konstruktor będziemy wywoływać w programach tak: ZNAK Obiekt('a'); Znaczy to: Utwórz w RAM obiekt klasy ZNAK pod nazwą "Obiekt" i wytłumacz mu, że jest znakiem 'a'. Konstruktor powinien pobierać od programu jeden argument typu char i przekazywać go obiektowi klasy ZNAK na jego pole danych znak_dany. Definicja konstruktora będzie zatem wyglądać tak: ZNAK::ZNAK(char x) { znak_dany = x; } Zakres dopuszczalnych znaków zawęzimy np. do kodów ASCII 65...90 (od A do Z). Jeśli użytkownik "nie trafi", ustawimy zawsze "*" (asterisk). Dodatkowo, dla "elegancji" zamienimy ewentualne małe litery na duże. ZNAK::ZNAK(char x) { znak_dany = x; if(znak_dany < 65 || znak_dany >122) znak_dany = '*'; if(znak_dany > 97) znak_dany -= 32; } A jeśli użytkownik nie zechce podać żadnego znaku i zda się na domyślność obiektu? Żaden problem, wystarczy do klasy ZNAK dodać bezparametrowy konstruktor domyślny. Konstruktory domyślne spełniają w C++ taką właśnie rolę: class ZNAK { char znak_dany; public: ZNAK(char); //Konstruktor zwykly ("jednoznakowy") ZNAK(void); //Konstruktor domyślny (bezparametrowy) ... Słowo void (tu opcjonalne) może nie wystąpić. Aby "kłuło w oczy", który konstruktor jest konstruktorem domyślnym (ang. default konstructor), większość programistów zapisuje to tak: class ZNAK { char znak_dany; public: ZNAK(char); ZNAK(); //Z daleka widać, że nic nie ma ! ... Definicja konstruktora bezparametrowego będzie wyglądać tak: ZNAK::ZNAK() { znak_dany = 'X'; } W zależności od sposobu zadeklarowania obiektu w programie C++ wywoła automatycznie albo konstruktor ZNAK(char), albo konstruktor domyślny ZNAK(): ZNAK obiekt; //Nie sprecyzowano jaki, konstruktor domyślny ZNAK obiekt('m'); //Wiadomo jaki, konstruktor jednoparametrowy Dzięki temu, że C++ "pedantycznie" sprawdza przed wywołaniem funkcji zgodność typów argumentów przekazywanych do funkcji (konstruktor to też funkcja) i porównuje typ argumentów z życzeniem programisty wyrażonym w prototypie - bezbłędnie rozpozna (mimo identycznej nazwy), którą funkcję należy zastosować. Dodajmy do klasy ZNAK deklaracje (prototypy) funkcji-metod: class ZNAK { char znak_dany; public: ZNAK(char); ZNAK(); void Pokaz_sie(); void Znikaj(); void Skacz(); }; i zdefiniujmy te metody. void ZNAK::Pokaz_sie(void) { cout << znak_dany << '\a'; } void ZNAK::Znikaj(void) { cout << "\b" << ' '; //'\b' == Back Space } void ZNAK::Skacz(void) { for(int i = 0; i < 100; i++) { gotoxy(rand()%50, rand()%50); cout << znak_dany; getch(); } } Jeśli implementację klasy ZNAK umieścimy w pliku nagłówkowym A:\ZNAK.H //_____________________________________________________________ # include # include # include class ZNAK { char znak_dany; public: ZNAK(char); ZNAK(); void Pokaz_sie(); void Znikaj(); void Skacz(); }; ZNAK::ZNAK() { znak_dany = 'X'; } ZNAK::ZNAK(char x) { znak_dany = x; if(znak_dany < 65 && znak_dany >122) znak_dany = '*'; if(znak_dany > 97) znak_dany -= 32; } void ZNAK::Pokaz_sie(void) { cout << znak_dany << '\a'; } void ZNAK::Znikaj(void) { cout << "\b" << ' '; //'\b' == Back Space } void ZNAK::Skacz(void) { for(int i = 0; i < 100; i++) { gotoxy(rand()%50, rand()%50); cout << znak_dany; getch(); } } //_____________ koniec pliku A:\INCLUDE\ZNAK.H _________________ to nasz program może wyglądać tak: [P103.CPP] # include void main() { char litera; clrscr(); cout << '\n' << "Podaj znak: "; cin >> litera; ZNAK Obiekt(litera); cout << "\nSTART" << "\n\n\n"; getch(); Obiekt.Pokaz_sie(); getch(); Obiekt.Znikaj(); getch(); Obiekt.Skacz(); ZNAK Obiekt2; //To bedzie domyslny 'X' Obiekt2.Skacz(); } I tu już widać pewne cechy nowoczesnego obiektowego stylu programowania. Tym razem sprwdzenie, czy słowo class można spokojnie zamienić na słowo struct pozostawim dociekliwym Czytelnikom.