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.
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 {...};
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 |
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.
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; }
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 <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? }
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.
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ę:
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 <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&); };
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 <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; }
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; }
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 to inaczej konwersja pomiędzy typami. Wyróżnia się dwa rodzaje rzutowania:
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ół 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:
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); }
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.
Punkt3D p3d(1,2,3); cout << p3d << endl;
#include "Kula.h" #include "Kolo.h" #include <iostream> using namespace std; int main(){ Kula k(0,0,0,10); }
Jeśli chcesz zobaczyć jak można wykorzystać dziedziczenie i polimorfizm w większych przykładach, zapraszam do zapoznania się z laboratorium Dziedziczenie i Polimorfizm -- Przykłady
Za tydzień kolokwium! Czy pamiętasz o nim?
Informacje organizacyjne i przykładowe pytania znajdują się tutaj.