====== 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]].