====== Dziedziczenie i polimorfizm ====== ===== Dziedziczenie ===== ====Klasy podstawowe i pochodne==== Dziedziczenie to jeden z najważniejszych mechanizmów programowania obiektowego. Polega on na ponownym wykorzystaniu kodu w taki sposób, że nowe klasy tworzone są na podstawie już istniejących dziedzicząc jej metody i pola i jednocześnie dodając nowe metody i nowe pola. {{.:dziedziczenie.png|Dziedziczenie}} Aby zapisać relację dziedziczenia w C%%++%% stosuje się następujący schemat: **class** //NazwaKlasyPochodnej// **:** //Tryb dziedziczenia// //NazwaKlasyBazowej// { //definicja klasy pochodnej// } Chcąc zapisać W C%%++%% relację z rysunku, moglibyśmy to zrobić w następujący sposób: class CzlonekUczelni{...}; class Student: public CzlonekUczelni{...}; class Pracownik: public CzlonekUczelni{...}; class PracownikNaukowy : public Pracownik{...} class PradownikAdministracyjny: public Pracownik{...}; //Dziedziczenie wielokrotne class Stażysta : public Student, PracownikNaukowy {...}; ====Dziedziczenie składowych==== Dostęp do składowych klasy bazowej może być ograniczony modyfikatorami dostępu zastosowanymi w klasie bazowej jak również typem dziedziczenia. Poniższa tabela przedstawia zależności pomiędzy trybem dziedziczenia a tym jak zmienia sie znaczenie modyfikatorów dostępu w klasie pochodnej. ^Modyfikator dostępu w klasie bazowej^Rodzaj dziedziczenia^^^ ^ ^public^protected^private^ ^public|//public// w klasie pochodnej.|//protected// w klasie pochodnej|//private// w klasie pochodnej| ^protected|//protected// w klasie pochodnej| //protected// w klasie pochodnej|//private// w klasie pochodnej| ^private|Ukryte w klasie pochodnej|Ukryte w klasie pochodnej|Ukryte w klasie pochodnej| ==== Konstruktory i destruktory==== Klasa pochodna tak naprawdę jest jednocześnie klasą bazową, dlatego nie można utworzyć jej obiektu bez wcześniejszego utworzenia obiektu klasy bazowej. Każdy konstruktor klasy pochodnej w pierwszej kolejności będzie wywoływał konstruktor klasy bazowej. {{.:dziedziczenie-punkt.png|Dziedziczenie}} Konstruktora klasy bazowej nie można wywołać w ciele konstruktora klasy pochodnej, ponieważ klasy pochodne dziedziczą pola klasy podstawowej, a wszystkie pola klasy muszą zostać utworzone **przed** utworzeniem danej klasy. Chcąc wywołać konstruktor klasy Punkt2D przez konstruktor klasy pochodnej Punkt3D stosujemy następujący zapis:#include "Punkt2D.h" #include "Punkt3D.h" Punkt3D::Punkt3D(double x, doubley, double _z) : Punkt2D(x,y) { z = _z; } ====Metody składowe==== Ponieważ każdy obiekt klasy pochodnej //jest// jednocześnie obiektem klasy bazowej nie ma żadnych różnic pomiędzy używaniem metod klasy bazowej i pochodnej w klasie pochodnej, czy tez na obiekcie klasy pochodnej. Należy jednak pamiętać o trybach dziedziczenia i ograniczeń jakie z tego wynikają. W przypadku gdy w klasie pochodnej zdefiniowana zostanie ponownie taka sama metoda jak w klasie bazowej, nastąpi tak zwane napisanie tej metody. Wywołując tą metodę na obiekcie klasy pochodnej będzie wywoływana metoda nowo zdefiniowana. Jeśli klasa bazowa ma przeciążone **operatory**, nie zostaną one odziedziczone przez klasę pochodną! #include using namespace std; class LiczbaRzeczywista{ protected: double re; public: LiczbaRzeczywista(double r){re=r;} void wypisz(){ cout << re << endl; } void powitaj(){ cout << "Czesc!" << endl; } LiczbaRzeczywista operator+(const LiczbaRzeczywista& r){ LiczbaRzeczywista rr(re+r.re); return rr; } }; class LiczbaZespolona : public LiczbaRzeczywista{ protected: double im; public: LiczbaZespolona(double re, double i):LiczbaRzeczywista(re){im=i;} void wypisz(){ cout << re << " + " << im << "i" << endl; } }; int main(){ LiczbaRzeczywista a(12); LiczbaZespolona b(23,5); a.powitaj(); //Czesc! a.wypisz(); //12 (a+a).wypisz(); //24 b.powitaj(); //Czesc! b.wypisz(); //23 + 5I (b+b).wypisz(); //Co się wypisze? } ===== Polimorfizm ===== Polimorfizm to możliwość różnej odpowiedzi na ten sam komunikat (wywołanie tej samej metody) przez obiekty różnych klas powiązanych dziedziczeniem. Poniższe podsekcje składają się na opis tego mechanizmu. ==== Funkcje wirtualne ==== Podczas dziedziczenia, klasy pochodne mogą nadpisywać metody swoich klas bazowych. Kiedy jednak dokonujemy rzutowania w górę (np. z LiczbaUrojona na LiczbaRzeczywista), nadpisane metody wracają z powrotem do swoich pierwotnych postaci (np. obliczanie pola przekroju powierzchni kuli za pomocą rzutowania na koło). Takie zachowanie nie zawsze jest pożądane. Załóżmy, że chcemy zbudować prosty program obsługujący bazę **pracowników** pewnej firmy. Zakładamy, że firma zatrudnia pracowników na **umowę o dzieło**, lub na **umowę o pracę**; wpływa to na obliczenie pensji netto każdego pracownika. Mamy zatem taką relację: {{.:umowa.png|Umowa - diagram UML}} Taka relacja umożliwia stworzenie tylko jednej klasy //Pracownik//, która będzie miała jedno pole //Umowa//, któremu z kolei będzie przypisywana abo //UmowaDzielo// albo //UmowaPraca//. W takim wypadku po rzutowaniu //UmowaDzielo// lub //UmowaPraca// na typ bazowy //Umowa//, musi być możliwe wywołanie metod odpowiednio obliczających wynagrodzenie netto. W tym celu stosowane są **funkcje wirtualne**. **Funkcja wirtualna**, to taka metoda klasy, która nadpisana w klasie pochodnej, nawet po rzutowaniu obiektu na typ bazowy zachowa swoją implementacje. Klasa bazowa z funkcją wirtualną będzie //polimorficzna// - w zależności od tego jaki obiekt klasy pochodnej był na nią rzutowany, taka implementacja metody zostanie uruchomiona. Przykład: #include #include using namespace std; class Umowa{ protected: double wynagrodzenieBrutto; public: Umowa(double pensja):wynagrodzenieBrutto(pensja){}; virtual double pobierzNetto(); double pobierzBrutto(); }; class UmowaDzielo: public Umowa{ public: UmowaDzielo(double pensja):Umowa(pensja){}; virtual double pobierzNetto(); }; class UmowaPraca: public Umowa{ public: UmowaPraca(double pensja):Umowa(pensja){}; virtual double pobierzNetto(); }; class Pracownik{ private: string imie,nazwisko,pesel; Umowa* umowa; public: Pracownik(string i,string n,string p,Umowa* u) :imie(i),nazwisko(n),pesel(p),umowa(u){}; Pracownik(const Pracownik&); ~Pracownik(){}; double pobierzPensje(); friend ostream& operator<<(ostream&,Pracownik&); }; ====Destruktory wirtualne==== Zwróć uwagę, że podobnie do metod zachowują się destruktory. Jeśli obiekt jest jawnie niszczony prze operator //delete// to wywoływany jest jego destruktor: #include ... int main(){ Umowa* umowa = new UmowaPraca(10000); // zostanie wywołany destruktor klasy Umowa, a nie UmowaPraca! // Jeśli UmowaPraca alokowałaby dodatkowo jakąś pamięć // NIE ZOSTAŁABY ONA ZWOLNIONA delete umowa; } Rozwiązaniem jest deklarowanie destruktora jako wirtualnego: class Umowa{ ... virtual ~Umowa(); }; class UmowaPraca :public Umowa{ ... virtual ~UmowaPraca(); }; int main(){ Umowa* umowa = new UmowaPraca(10000); // zostanie wywołany destruktor klasy UmowaPraca delete umowa; } ==== Klasy abstrakcyjne==== Czasem w klasie bazowej implementowanie jakiejś metody jest bezcelowe. W naszym przykładzie z pracownikami implementowanie metody //pobierzNetto// w klasie //Umowa// nie ma sensu, bo nie wiadomo jaki jest to typ umowy. Metody takie można zadeklarować jako czysto wirtualne:class UmowaDzielo: public Umowa{ protected: double wynagrodzenieBrutto; public: Umowa(double pensja):wynagrodzenieBrutto(pensja){}; // Metoda czysta. Nie posiada implementacji w klasie bazowej. // MUSI jednak być zaimplementowana w pochodnej (lub ponownie // zadeklarowana jako czysta virtual double pobierzNetto() = 0; }; Klasę zawierającą choć jedną metodę czystą nazywamy **klasą abstrakcyjną**. Nie jest możliwe utworzenie obiektu takiej klasy! Służy ona jedynie jako klasa bazowa dla innych klas. Swego rodzaju interfejs. =====Rzutowanie===== Rzutowanie to inaczej konwersja pomiędzy typami. Wyróżnia się dwa rodzaje rzutowania: * rzutowanie w górę (z klasy pochodnej na klasę bazową) * rzutowanie w dół (Z klasy bazowej na klasę pochodną) ====Rzutowanie w górę==== Rzutowanie w górę można wykonywać niejawnie, ponieważ każdy obiekt klasy pochodnej //jest// jednocześnie obiektem klasy bazowej: void foore(LiczbaRZeczywista r){ cout << "Wyswietlamrzeczywista "; r.wypisz(); } int main(){ LiczbaZespolona z(12,4); foore(z); //nastąpi rzutowanie w górę } ====Rzutowanie w dół==== Rzutowanie w dół oznacza rzutowanie z klasy bazowej na pochodną. Jeśli nie zostanie przeciążony odpowiedni operator rzutowania, operację taką należy wykonywać przy użyciu jednego z poniższych operatorów: ^Operator^Opis^ ^static_cast(wyrazenie)|Sprawdzanie poprawności typów podczas rzutowania wykonywane jest podczas kompilacji| ^const_cast(wyrazenie)|Może być stosowany tylko ze wskaźnikami lub referencjami. Pozwala na anulowanie stałości wskaźnika obiektu (//const//).| ^dynamic_cast(wyrazenie)|Może być stosowany tylko ze wskaźnikami lub referencjami.Pozwala na rzutowanie w górę (dla wszystkich klas) i na rzutowanie w dół (tylko dla klas polimorficznych), sprawdzając czy obiekt który rzutujemy jest faktycznie kompletnym obiektem klasy na którą chcemy go rzutować. Operator przeprowadza ten test podczas działania programu; Jeśli konwersja nie może się odbyć, zwracany jest wskaźnik null ( w przypadku referencji rzucany jest wyjątek //bad_cast//)| ^reinterpretc_cast(wyrazenie)|Stosowanie tego operatora jest potencjalnie bardzo niebezpieczne. Może on rzutować dowolny typ na dowolny inny typ, nawet jeśli oba typy nie mają ze sobą nic wspólnego.| Poniżej przedstawiono przykłady użycia operatorów i różnice w ich stosowaniu: class A { public: virtual void foo() = 0; }; class B: public A{ public: virtual void foo(){} }; class C { public: void bar(){}; }; class D: public C{ public: void otherbar(){} }; int main(){ // static_cast - zwykłe rzutowanie w górę D* ptrD = new D(); C* ptrC = static_cast(ptrD); // const_cast - pozbywamy się modyfikatora const const C* constPtrC = new C(); C* ptrCC = const_cast(constPtrC); // dynamic_cast - rzutowanie w dół A* ptrA = new B(); B* ptrB = dynamic_cast(ptrA); // reinterpret_cast - rzutowanie pomiedzy kompletnie niepowiazanymi klasami B* ptrBB = new B(); D* ptrDD = reinterpret_cast(ptrBB); } ====== Ćwiczenia ====== **UWAGA**\\ Przesyłając rozwiązania zadań mailowo należy zamieścić (w formie komentarza w kodzie lub w treści maila) odpowiedzi na problemy postawione w zadaniach 2-4 **wraz z uzasadnieniami**. Zadanie bez właściwego uzasadnienia nie będzie zaliczone. - [1 plus] Przetestuj przykład z sekcji [[#metody_skladowe|Metody składowe]] - **[1 punkt] Wykorzystując klasę [[.:klasy1#deklaracja_klasy|Punkt]], napisz klasę Punkt3D dziedziczącą po niej i implementującą dodatkowo metodę //double distance(Punkt3D)//. W każdym z konstruktorów i destruktorów klas Punkt i Punkt3D wypisz na ekran jakąś wiadomość i zaobserwuj w jakiej kolejności wywołują się konstruktory i destruktory.** - **[1 punkt] Mając dwa obiekty, jeden klasy Punkt a drugi klasy Punkt3D o nazwach //punkt2d// i //punkt3d//, wywołaj punkt2d.distance(punkt3d). Co sie stało?** - **[1 punkt] W klasie Punkt2D istnieje przeciążony operator wpisywania do strumienia ("<<"). Co się stanie jeśli będziesz chciał wypisać obiekt klasy Punkt3D w następujący sposób:** Punkt3D p3d(1,2,3); cout << p3d << endl; - [3 plusy] Napisz dwie klasy: Kolo i Kula. Klasa Kolo powinna być klasą bazową dla klasy Kula. * W klasie Kolo powinny znaleźć się następujące pola i metody: * **double** x, y, r - określające odpowiednio współrzędne środka koła i jego promień. * **double** pole() - obliczająca pole koła * W klasie Kula powinny znaleźć się następujące pola i metody: * **double** z - określajaca współrzędną przestrzenną środka kuli * **double** pole() - obliczająca pole powierzchni kuli * W jaki sposób mając następującą funkcję //main// można obliczyć pole przekroju zdefiniowanej tam kuli płaszczyzną przechodzącą przez środek kuli, wywołując wyłącznie jedną metodę? #include "Kula.h" #include "Kolo.h" #include using namespace std; int main(){ Kula k(0,0,0,10); } - [2 plusy] Dopisz brakujące metody z przykładu [[#funkcje_wirtualne|Funkcje wirtualne]] i uruchom program. - **[2 punkty] Napisz klasę abstrakcyjną //Ksztalt//, która będzie posiadała jedna czystą metodę wirtualną //rysuj//. Następnie napisz kilka klasę dziedziczących po tej klasie (//Trójkąt, Kwadrat, Koło//) i odpowiednio implementujących metodę //rysuj//. Metoda powinna rysować kształty w trybie tekstowym. \\ Zadeklaruj listę, która przechowuje wskaźniki na obiekty klasy //Ksztalt// i uzupełnij ją losowo //Kolami, Kwadratami// albo //Trojkatami//. Następnie wywołaj na każdym obiekcie z listy metodę //rysuj//. Jaki jest tego efekt?** Jeśli chcesz zobaczyć jak można wykorzystać dziedziczenie i polimorfizm w większych przykładach, zapraszam do zapoznania się z laboratorium [[.:dziedziczenie-ex|Dziedziczenie i Polimorfizm -- Przykłady]] Za tydzień kolokwium! Czy pamiętasz o nim? Informacje organizacyjne i przykładowe pytania znajdują się [[..:start#kolokwium|tutaj]].