|
|
pl:dydaktyka:jimp2:2015:labs:dziedziczenie [2015/03/08 15:54] kkutt utworzono |
pl:dydaktyka:jimp2:2015:labs:dziedziczenie [2019/06/27 15:50] |
====== 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: | |
<code cpp> | |
class CzlonekUczelni{...}; | |
class Student: public CzlonekUczelni{...}; | |
class Pracownik: public CzlonekUczelni{...}; | |
class PracownikNaukowy : public Pracownik{...} | |
class PradownikAdministracyjny: public Pracownik{...}; | |
//Dziedziczenie wielokrotne - patrz Lab9 | |
class Stażysta : public Student, PracownikNaukowy {...}; | |
</code> | |
| |
| |
====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:<code cpp>#include "Punkt2D.h" | |
#include "Punkt3D.h" | |
| |
Punkt3D::Punkt3D(double x, doubley, double _z) : Punkt2D(x,y) { | |
z = _z; | |
} | |
</code> | |
| |
| |
| |
====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ą! | |
| |
<code cpp> | |
#include <iostream> | |
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? | |
} | |
</code> | |
| |
===== 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:<code cpp> | |
#include <iostream> | |
#include <list> | |
| |
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&); | |
};</code> | |
| |
====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:<code cpp> | |
#include <iostream> | |
... | |
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; | |
} | |
</code> | |
| |
Rozwiązaniem jest deklarowanie destruktora jako wirtualnego:<code cpp> | |
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; | |
} | |
</code> | |
| |
==== 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:<code cpp>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; | |
}; | |
</code> | |
| |
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: | |
<code cpp> | |
| |
void foore(LiczbaRZeczywista r){ | |
cout << "Wyswietlamrzeczywista "; | |
r.wypisz(); | |
} | |
| |
int main(){ | |
LiczbaZespolona z(12,4); | |
| |
foore(z); //nastąpi rzutowanie w górę | |
| |
} | |
| |
</code> | |
| |
====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<nowy_typ>(wyrazenie)|Sprawdzanie poprawności typów podczas rzutowania wykonywane jest podczas kompilacji| | |
^const_cast<nowy_typ>(wyrazenie)|Może być stosowany tylko ze wskaźnikami lub referencjami. Pozwala na anulowanie stałości wskaźnika obiektu (//const//).| | |
^dynamic_cast<nowy_typ>(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<nowy_typ>(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: | |
<code cpp> | |
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<C*>(ptrD); | |
| |
// const_cast - pozbywamy się modyfikatora const | |
const C* constPtrC = new C(); | |
C* ptrCC = const_cast<C*>(constPtrC); | |
| |
// dynamic_cast - rzutowanie w dół | |
A* ptrA = new B(); | |
B* ptrB = dynamic_cast<B*>(ptrA); | |
| |
// reinterpret_cast - rzutowanie pomiedzy kompletnie niepowiazanymi klasami | |
B* ptrBB = new B(); | |
D* ptrDD = reinterpret_cast<D*>(ptrBB); | |
} | |
</code> | |
| |
| |
====== Ćwiczenia ====== | |
- [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 kalsy 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:** <code cpp>Punkt3D p3d(1,2,3); | |
cout << p3d << endl;</code> | |
- [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ę? <code cpp>#include "Kula.h" | |
#include "Kolo.h" | |
#include <iostream> | |
| |
using namespace std; | |
| |
int main(){ | |
Kula k(0,0,0,10); | |
} | |
</code> | |
- [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 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?** | |
| |
<WRAP center round info 60%> | |
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]] | |
</WRAP> | |
| |
<WRAP center round important 60%> | |
Za tydzień kolokwium! Czy pamiętasz, że: | |
* obowiązuje na nim materiał ze wszystkich laboratoriów? | |
* kolokwium rozwiązywane jest na kartkach? | |
* pytania będą teoretyczne (dotyczące treści zawartych we wprowadzeniach do laboratorium) i praktyczne (związane z napisaniem kilku linii kodu)? | |
</WRAP> | |
| |