Dziedziczenie

Omówimy teraz jeden z podstawowych konceptów programowania obiektowego, tj. dziedziczenie.

W założeniu dziedziczenie ma umożliwić rozszeżanie możliwości istniejących klas poprzez tworzenie nowych klas zależnych od już istniejących. Dzięki temu powtarzalne elementy są wykorzystywane wielokrotnie (wspoldzielenie kodu). W zalożeniu dziedziczenie ma umożliwić latwiejsze zarządzanie poprzez wprowadzenie hierarchi klas. Hierarchię pobierznie wyjaśnimy na przykladzie kształtów:
kształt uogólnia prostokąty, a te uogólniają kwadraty, tzn. kształt będzie bazowy dla prostokątów i kwadratów, itd.

Klasę po której będziemy dziedziczyć nazwiemy bazową klase dziedziczącą natomiast pochodną. W przykładzie powyżej kształty będa bazowe, a kwadraty pochodne.

Dziedziczenie mat umozliwić abstrakcujne podejście do zadania programistycznego poprzez interpretowania klas dziedziczących jak klas bazowych w zaleznosci od kontekstu, w którym pracujemy (polimorfizm). Będzie tak ponieważ, obiekty klas pochodnych będą jednoczesnie obiektami klasy bazowej (będą miały wspolnego przodka - bazę), ale nie odwrotnie.

Przykład bez dziedziczenia

Napiszemy przykładowy program, w którym pojawia sie niezwiązane ze sobą klasy i postaramy się w kolejnych krokach przystosować nasz kod do wykorzystania dziedziczenia.

Zadanie rolniecze. Utowrzymy klasy opisujące zwierzęta na farmie. Każde może wydawać dźwiek i się poruszać. Na przykład o tak:

In [1]:
#include <iostream>
using namespace std;
Out[1]:

In [2]:
class kura{
    public:
    void dzwiek(){cout << "KoKoKo" << endl;}
    void chodz(){/* implementuje poruszanie sie */}
};
class swinia{
    public:
    void dzwiek(){cout << "HrumHrumHrum" << endl;}
    void chodz(){/* implementuje poruszanie sie */}
};
class kon{
    public:
    void dzwiek(){cout << "IchaCha" << endl;}
    void chodz(){/* implementuje poruszanie sie */}
};
Out[2]:

Zarządzanie "losem" obiektów nie jest w tym przypadku wygodne. Musimy stworzyć każdy obiekt oddzielnie, przechowywać je w oddzielnych kolekcjach i zastanawiać sie jakie kto metody ma wywołać:

In [5]:
cout << "Farma wuja Sama" << endl;

kura ktab[5];
swinia stab[2];
kon k;

//wydajemy dzwieki
for(int i=0; i<5; ++i)
    ktab[i].dzwiek();
for(int i=0; i<2; ++i)
    stab[i].dzwiek();
k.dzwiek();
Farma wuja Sama
KoKoKo
KoKoKo
KoKoKo
KoKoKo
KoKoKo
HrumHrumHrum
HrumHrumHrum
IchaCha
Out[5]:
(void) @0x7fe7fca2cba8

Przedstawimy teraz koncepty związane z dziedziczeniem w języku C++, częśc może sie przekładać na inne języki obiektowe (C#, Java, Python, inne), częśc zupelnie nie.

Podstawy

Składnia

Aby zadeklarować dziecziczenie jednej klasy po drógiej napiszemy:

In [ ]:
class nazwa_klasy_pochodnej : [operator widoczności] nazwa_klasy_bazowej{
    //cialo klasy
};

np. B dziedziczy po A

In [ ]:
class A {};
class B : public A {};

Co na diagramie UML(Unified Modeling Language) zaznaczylibyśmy przez otwartą strzalką, o tak:

Screenshot%20from%202019-11-01%2012-53-44.png

Nowy operator widoczności - protected

Do teraz ograniczaliśmy dostęp do składowych klasy poprzez wykorzystanie operatorów widoczności public i private. Dla przypomnienia, to co znajdowało się w sekcji public było dostepne dla wszystkich, a to co znajdowało się w sekcji private było chronione i próby manipulacji kończyly się blędem kompilacji. Z okazji dziedziczenia dochodzi nam trzeci operator, tj. protected. W zasadzie działa jak private (tj. składowe nim objęte sa niedostępne do manipulacji z zewnątrz) z tą roznicą, że w przypadku dziedziczenia składowe sa dostepne dla obiektu dziedziczacego. Popatrzmy na przykład:

In [6]:
class A {
public :
  int a ;
protected :
  int b ;
private :
  int c ;
};
class B : public A {
public :
  void fun () { a =8; b =4; c =5;}
};
input_line_9:12:29: error: 'c' is a private member of 'A'
  void fun () { a =8; b =4; c =5;} // c jest private !
                            ^
input_line_9:7:7: note: declared private here
  int c ;
      ^

Próbowaliśmy w klasie dziedziczącej zmienić wartośc skladowej prywatnej c. Tak będzie lepiej:

In [1]:
class A {
public :
  int a ;
protected :
  int b ;
private :
  int c ;
};
class B : public A {
public :
  void fun () { a =8; b =4; /*c =5*/;} // c jest private !
};
Out[1]:

Operator widoczności w deklaracji dziedziczenia

W składni deklaracji dziedziczenia wystepuje operator widoczności. Powoduje on zmianę widoczności atrybutów w klasie dziedziczącej w nastepujący sposób:

  • public - bez zmian:
    • public -> public
    • protected -> protected
    • private -> private
  • protected
    • public -> protected
    • protected -> protected
    • private -> brak dostępu
  • private

    • public -> private
    • protected -> private
    • private -> brak dostepu
  • brak operatora -> domyslnie jak private

Najczęściej (zawsze) bedziemy wykorzystywać modyfikator public.

Popatrzmy na przykład. Klasa A jest klasa bazową. Posiada 3 atrybuty, każdy w innej sekcji:

In [2]:
class A {
public :
  int a ;
protected :
  int b ;
private :
  int c ;
};
Out[2]:

Klasy B, C i D dziedziczą po A w rożny sposób:

In [3]:
class B : public A {
public :
  void fun () { a =8; b =4;/*c =5;*/} // c jest private !
};
class C : protected A {
public :
  void fun () { a =8; b =4;/*c =5;*/} // c jest private !
  void fun2() {cout << a << " " << b << endl;}
};
class D : A {//private!
public :
  void fun () { a =8; b =4;/*c =5;*/} // c jest private !
  void fun2() {cout << a << " " << b << endl;}
};
Out[3]:

B ma dostęp do wszystkiego w A, poza sekcja private:

In [4]:
B b;
b.fun();
cout << b.a << endl;
//cout << b.b << " " << b.c << endl;
8
Out[4]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7fcf481c1e60

C może użyć wszystkiego w A poza private, ale nie może "wystawić" do użytku nic z A:

In [5]:
C c;
c.fun();
c.fun2()
8 4
Out[5]:
(void) @0x7fcf3dff9ba8
In [6]:
cout << c.a << endl;
input_line_9:2:10: error: cannot cast 'C' to its protected base class 'A'
 cout << c.a << endl;
         ^
input_line_5:6:12: note: declared protected here
 class C : protected A {
           ^~~~~~~~~~~
input_line_9:2:12: error: 'a' is a protected member of 'A'
 cout << c.a << endl;
           ^
input_line_5:6:12: note: constrained by protected inheritance here
 class C : protected A {
           ^~~~~~~~~~~
input_line_4:3:7: note: member is declared here
  int a ;
      ^

D może jeszcze mniej, żeby to pokazać powinniśmy wykonać dziedziczenie po C i D i sprawdzić dostępność atrybutów A z wewnątrz klas dziedziczących.

In [7]:
D d;
d.fun();
d.fun2();
8 4
Out[7]:
(void) @0x7fcf3dff9ba8

Rozmiar obiektu - operator sizeof

Konsekwencja dziedziczenia klas jest rozszerzanie funkcjonalnosci i danych, tj. klasa pochodna roszerza bazową. W konsekwencji obiekty klas dziedziczących bedą potrzebować więcej miejsca w pamieci do ich przechowania. Popatrzmy na przykład, w którym klasa B dziedziczy po A, obie przechowują tabliczę 1024 char (1B każdy) oraz po jednym int (4B):

In [2]:
class A {
public :
  char tabA [1024];
  int a ;
};
class B : public A {
public :
  char tabB [1024];
  int b ;
};
Out[2]:

In [3]:
A a;
B b;
cout << "Sizes in bytes: A:" << sizeof ( a ) <<" B:"<< sizeof ( b ) << endl;
Sizes in bytes: A:1028 B:2056
Out[3]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f1238c21e60

Widzimy, że rozmiary instancji A i B to odpowiednie 1028 i 2056 B. Co jeżeli chcemy przechować te instancje w tablicy. Rozpatrzyć możemy takie mozliwości:

In [4]:
A tab_A[2];
B tab_B[2];
Out[4]:

Na początek sprawdźmy odległości między elementami tablic. Zapiszemy w postaci long (to taki durzy int zdolny do przechowania odpowiednio durzej wartości, a skoro pamięci w nowoczesnym komputerze jest durzo zwykły int nie wystarczy) położenie tab_A i jej elementu [1]. Wykorzystamy operator adresu \&

In [ ]:
long pos_tabA = (long)tab_A, pos_tabA1 = (long)&tab_A[1];
In [18]:
cout << pos_tabA1 - pos_tabA << endl;
1028
Out[18]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f1238c21e60

Czyli morzemy przechowywać w tablicy tab_A obiekty o rozmiarze 1028 B. Nie powinno nas to dziwić, taki jest w końcu rozmiar A. A co z tab_B:

In [19]:
long pos_tabB = (long)tab_B, pos_tabB1 = (long)&tab_B[1];
Out[19]:

In [23]:
cout << pos_tabB1 - pos_tabB << endl;
2056
Out[23]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f1238c21e60

A w tablicy tab_B obiekty o rozmiarze 2058 B. Co nas ponownie nie dziwi.

Naszym kolejnym krokiem jest próba wykorzystania polimorfizmu czyli faktu, że B jest rozszeżeniem A (tzn. jest A i czymś jeszcze):

To zadziała, choć jest niepoprawne ze względu na rozmiar przechowywanych w tab_A elementów. tzn. alement tab_A[1] "wystaje" poza zarezerwowaną pamięć!

Uwaga: należy unikać takiego zapisu:

In [27]:
tab_A[0] = A();
tab_A[1] = B();
Out[27]:
(A &) @0x7f1236ec3404

Może więcale tak:

In [26]:
tab_B[0] = A();
tab_B[1] = B();
input_line_29:2:11: error: no viable overloaded '='
 tab_B[0] = A();
 ~~~~~~~~ ^ ~~~
input_line_4:7:8: note: candidate function (the implicit copy assignment operator) not
      viable: no known conversion from 'A' to 'const B' for 1st
      argument
 class B : public A {
       ^
input_line_4:7:8: note: candidate function (the implicit move assignment operator) not
      viable: no known conversion from 'A' to 'B' for 1st
      argument

Ale to nie zadziała (dlaczego?).
Jedynym dopuszczalnym działaniem jest przechowanie tablicy wskaźników i wykorzystanie operatorów new i delete, w taki sposób:

In [28]:
A* tab[2];
tab[0] = new A();
tab[1] = new B();

// Działania na tab[0] i tab[1]

delete tab[0];
delete tab[1];
Out[28]:
(void) @0x7f12267faba8

Wielodziedziczenie

Zagadnienie diamentu ...

Język C++ umożliwia dziedziczenie po więcej niż jednej klasie. Może to prowadzić do struktury hierarchi zwanej diamentem. Jest to przypadek w, której klasa pochodna dziedziczy po dwóch klasach bazowych, które znowu dziedziczą po wspólnej klasie bazowej. Nie jest wówczas jasne w jaki sposób klasa znajdująca się najniżej w hierarchi ma się dostać do atrybutów klasy najwyżeszej. Przykładowy schemat UML takiej struktury wyglągda następująco:

Screenshot%20from%202019-11-01%2014-53-48.png

Uwaga: W części języków będącymi w jakims sensie pochodnymi C++ uniemozliwia się w ogóle dziedziczenie wielokrotne by uniknąc tego problemu.

Postaramy się teraz pokazać konsekwencję takiego stanu, a następnie jak język C++ rozwiązuje ten problem. Hierarchia jest jak na ilustracji, po klasie bazowej A dziedziczą klasy B i C. Klasa D dziedziczy zarowno po B i C. Klasy posiadają pewne atrybuty, mamy więc:

In [2]:
class A {
  char buff [1024];
public :
  void show () {cout << "Wywołano medodę A" << endl;}
};
class B : public A {int b;};
class C : public A {int c;};
class D : public B , public C { int d;};
Out[2]:

Rozmiary instancji poszczególnych klas wyglądaja następująco:

In [3]:
cout << "A: " << sizeof(A) << " B: " << sizeof(B) << " C: " << sizeof(C) << endl;
cout << "ale D: " << sizeof(D) << endl;
A: 1024 B: 1028 C: 1028
ale D: 2060
Out[3]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f2c8f4fce60

A zajmuje 1024B, i wiemy dlaczego. B i C dodają po 4B ze wzgledu na swoje atrybuty. Natomiast D zdaje się zawierać 2x1024 + 4 + 4 + 4 = 2060B. Czyli zawiera w sobie dwie klasy A! Być może tego własnie chcemy (raczej nie), ale jak teraz jednoznacznie dostać się do atrybutów A?

In [4]:
A a; a.show();
B b; b.show();
C c; c.show();
Wywołano medodę A
Wywołano medodę A
Wywołano medodę A
Out[4]:
(void) @0x7f2c8958cba8

ale:

In [5]:
D d;
d.show();
input_line_8:3:3: error: non-static member 'show' found in multiple base-class subobjects
      of type 'A':
    class D -> class B -> class A
    class D -> class C -> class A
d.show();
  ^
input_line_4:4:8: note: member found by ambiguous name lookup
  void show () {cout << "Wywołano medodę A" << endl;}
       ^

Nie możemy więc bezpośrednio dostać sie do atrybutu klasy bazowej A, ponieważ nie jest jasne czy dostajemy się do A przez B, czy przez C. Zadziała natomiast:

In [7]:
b.B::show();
b.C::show();
Wywołano medodę A
Wywołano medodę A
Out[7]:
(void) @0x7f2c8958cba8

... i jego rozwiązanie

Aby jednoznacznie rozwiazać problem wielodziedziczenia wprwadzono dziedziczenie wirtualne. Deklaruje się je przy użyciu slówkla kluczowego virtual:

In [ ]:
class B : public virtual A {};

a jego celem jest poinformowanie kompilatora, że dostęp do atrybutów powinien byc jednoznaczny oraz, że klasa bazowa powinna zostać wykorzystana jedynie raz. Zobaczmy to na przykładzie. Hierarchia klas jest jak poprzednio:

In [2]:
class A {
  char buff [1024];
public :
  void show () {cout << "Wywołano medodę A" << endl;}
};
class B : public virtual A {int b;};
class C : public virtual A {int c;};
class D : public B , public C {int d;};
Out[2]:

teraz:

In [3]:
cout << "A: " << sizeof(A) << " B: " << sizeof(B) << " C: " << sizeof(C) << endl;
cout << "ale D: " << sizeof(D) << endl;
A: 1024 B: 1040 C: 1040
ale D: 1056
Out[3]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7ff302b7ce60

Zauważamy, że instancje B i C są większe niż poprzednio o 12B, jest to cena jaką płacimy za wirtualnośc dziedziczenia. Natomiast rozmiar D to 1056, więc też odrobinę więcej niż proste sumowanie atrybutów A, B i C (1024 + 4 + 4) oraz atrybutów D (4).

Dostęp do atrybutów A jest teraz jednoznaczny:

In [4]:
A a; a.show();
B b; b.show();
C c; c.show();
D d; d.show();
Wywołano medodę A
Wywołano medodę A
Wywołano medodę A
Wywołano medodę A
Out[4]:
(void) @0x7ff2fcc0cba8

Dostęp do atrybutów - unifikacja interfejsu

Zacznieny teraz unifikować dostep do atrybutów klasy bazowej i dziedziczących. Wróćmy do początkowego przykładu farmerskiego. Zwierzęta na naszej famie rozpatrzymy jako dziedziczące po klasie bazowej bydle, damy im też umiejątność jedzenia i wydawanie dzwieku.

In [1]:
#include <iostream>
using namespace std;
Out[1]:

In [2]:
class bydle {
public :
  void jedz () {cout << "jedz() not implemented for base class bydle!!" << endl;}
  void dzwiek(){cout << "dzwiek() not implemented for base class bydle!!" << endl;}
};

class kura : public bydle {
public :
  void dzwiek() {cout << " Kokoko" << endl;}
  void jedz() {cout << " Kura dziobie i grzebie" << endl;};
};
class krowa : public bydle {
public :
  void dzwiek() {cout << " Muuu" << endl;}
  void jedz() {cout << " Krowa przezuwa" <<endl;}
};
Out[2]:

Wszystkie trzy klasy definiują obiekty i można je stworzyć i użyc. Rezultat ponizszego nie powinien być dla nas zdziwieniem:

In [3]:
bydle b;
b.jedz();
b.dzwiek();

kura ku;
ku.jedz();
ku.dzwiek();

krowa kr;
kr.jedz();
kr.jedz();
jedz() not implemented for base class bydle!!
dzwiek() not implemented for base class bydle!!
 Kura dziobie i grzebie
 Kokoko
 Krowa przezuwa
 Krowa przezuwa
Out[3]:
(void) @0x7f5d60dafba8

Klasy kura i krowa dziedziczą po klasie bydle spróbujemy więc przenieść naszą relację na wyższy poziom i nie wdawać się w to jakim konkretnie typem sie zajmujemy. Wiemy, że w tablicy przechowywać powinniśmy wskażniki:

In [5]:
bydle* tab[3];
tab[0] = new bydle();
tab[1] = new kura();
tab[2] = new krowa();
Out[5]:
(bydle *) 0x7f5d408a2420

Natomiast taki zabieg pozwoli na wywołanie metod:

In [6]:
for( int i=0; i<3; ++i){
    tab[i]->jedz();
    tab[i]->dzwiek();
}
jedz() not implemented for base class bydle!!
dzwiek() not implemented for base class bydle!!
jedz() not implemented for base class bydle!!
dzwiek() not implemented for base class bydle!!
jedz() not implemented for base class bydle!!
dzwiek() not implemented for base class bydle!!
Out[6]:

ale tylko dla klasy bazowej! Nie jest to to co byśmy chcieli zobaczyć. Oczywiście można wykonać rzutowanie na odpowiednie typy, ale było by to niewygodne i w zasadzie bezsensu.

Metody wirtualne

Co mozemy więc zrobić?
Dostęp do prawidlowej metody realizuję się poprzez dodanie slowa kluczowego virtual do deklaracji metody. Zabieg ten pozwala na wywołanie metody klasy pochodnej z poziomu klasy bazowej. Nie jest to zabieg "darmowy", a obarczony pewnycm kosztem pamięci i czasu ponieważ jest realizowany nie w czasie kompilacji przez dowiązanie metod, a w czasie wykonania programu i wymaga stworzenia tablicy metod wirtualnych.

In [2]:
class bydle {
public :
  virtual void jedz () {cout << "jedz() not implemented for base class bydle!!" << endl;}
  virtual void dzwiek(){cout << "dzwiek() not implemented for base class bydle!!" << endl;}
};

class kura : public bydle {
public :
  virtual void dzwiek() {cout << " Kokoko" << endl;}
  virtual void jedz() {cout << " Kura dziobie i grzebie" << endl;};
};
class krowa : public bydle {
public :
  virtual void dzwiek() {cout << " Muuu" << endl;}
  virtual void jedz() {cout << " Krowa przezuwa" <<endl;}
};
Out[2]:

Teraz:

In [3]:
bydle* tab[3];
tab[0] = new bydle();
tab[1] = new kura();
tab[2] = new krowa();

for( int i=0; i<3; ++i){
    tab[i]->jedz();
    tab[i]->dzwiek();
}
jedz() not implemented for base class bydle!!
dzwiek() not implemented for base class bydle!!
 Kura dziobie i grzebie
 Kokoko
 Krowa przezuwa
 Muuu
Out[3]:

Widzimy, że wywołano metody klas pochodnych, czyli to czego chcieliśmy.

Kolejność wywolania metod konstruktora i destruktora

Konstruktor

W chwili tworzenia obiektu pochodnego obiekt bazowy (subobiekt) powinien już istnieć. Tak więc konstruktor bazowy zostanie wywołany najpierw. Powinniśmy zwrócić na ten fakt uwagę pisząc wlasne konstruktory dla klas pochodnych i wywoływać konstruktory klas bazowych w liście inicjalizacyjnej.

In [1]:
class A {
public :
  A () { cout << "Konstruktor A " << endl ;}
  A ( int a ) { cout << "konstruktor parametryczny A " << a << endl;}
};

class B : public A {};
Out[1]:

Stwóżmy teraz obiekty A i B:

In [2]:
A a, a1(1);
B b;
Konstruktor A 
konstruktor parametryczny A 1
Konstruktor A 
Out[2]:

Oba wywołały konstruktor bazowy A. Co jeżeli jawnie stworzymy konstruktor B. Wówczas powinniśmy wywołać odpowiadający nam konstruktor klasy bazowej w liście inicjalizacyjnej:

In [2]:
class A {
public :
  A() { cout << "Konstruktor A " << endl ;}
  A(int a) { cout << "konstruktor parametryczny A " << a << endl;}
};

class B : public A {
public :
  B() : A() { cout << "Konstruktor B " << endl ;}
  B(int a) : A(a) {cout << "Konstruktor parametryczny B " << a << endl;}
};
Out[2]:

Stwórzmy kilka instancji B:

In [3]:
B b, b1(1);
Konstruktor A 
Konstruktor B 
konstruktor parametryczny A 1
Konstruktor parametryczny B 1
Out[3]:

Destruktor

O ile konstrując obiekt jawnie wywołujemy kontruktor klasy pochodnej (wiemy co chcemy utworzyć), o tyle w przypadku wywołania destruktora możliwe, że dysponujemy jedynie odniesieniem do typu bazowego (nie koniecznie wiemy i nie obchodzi nas co niszczymy). Dodatkowo, to klasa pochodna determinowała wykorzystywane przez obiekt zasoby i ich zwolnienia możemy dokonać jedynie z poziomu obiektu pochodnego. Aby to umożliwić, konieczne będize wykorzystanie mechanizmu funkcji wirtualnych, tj. destruktor uczynimy wirtualnym.

Uwaga: dobrą praktyką jest czynić destruktor wirtualnym.

Rozważmy następujący przykład, w którym klasa B dziedziczy po klasie A, obie definiują destruktory:

In [2]:
class A{
    public:
    A() {cout << "Konstruktor A" << endl;}
    ~A(){cout << "Destruktor A" << endl;}
};
class B : public A{
    public:
    B(int n) : A() {
        cout << "Konstruktor B - stworzenie tablicy " << n << " elementów " << endl;
        ptab = new int[n];
    }
    ~B() {
        cout << "Destruktor B - zniszczenie tablicy " << endl;
        delete [] ptab;
    }
    private:
    int* ptab;
};
Out[2]:

Stwóżmy kilka instancji, dodatkowo ograniczymy zakres obowiązywania by zapewnić wywołanie destruktora:

In [3]:
{
    A a;
    B b(5);
}
Konstruktor A
Konstruktor A
Konstruktor B - stworzenie tablicy 5 elementów 
Destruktor B - zniszczenie tablicy 
Destruktor A
Destruktor A
Out[3]:

Jak widzimy w obu przypadkach wywołano destruktor klasy bazowej A, a w przypadku obiektu typu B wywolany zostal najpierw destruktor B a następnie A. Zdaje się, że wszystko jest w porządku, ale jest tak tylko dlatego, że w sposób jawny operowaliśmy typem B. Co stanie się, jeżeli dysponujemy jedynie wskaźnikiem na typ bazowy:

In [5]:
A* tab[2];
tab[0] = new B(2);
tab[1] = new B(5);

// Działamy z tab

delete tab[0];
delete tab[1];
Konstruktor A
Konstruktor B - stworzenie tablicy 2 elementów 
Konstruktor A
Konstruktor B - stworzenie tablicy 5 elementów 
Destruktor A
Destruktor A
Out[5]:
(void) @0x7f27db7fcba8

Wywołany został jedynie destruktor A! Destruktor B nie został wywołany, a to znaczy, że niezwolniona została zaalokowana wcześniej pamięć. Może sie to wydawać mało ważne (pamięci jest dużo!), ale tak nie jest. Np. w przypadku wyspecjalizowanego sprzętu, lub gdy tworzenie i destrukcja obiektów następuje często, a jednoczesnie zajmują one dużo zasobów, czy w końcu gdy zniszczony własnie obiekt był właścicielem istotengo zasobu (dostępu do urządzenia).

Aby zapewnić wywolanie destruktora klasy pochodnej, uczyńmy go wirtualnym:

In [2]:
class A{
    public:
    A() {cout << "Konstruktor A" << endl;}
    virtual ~A(){cout << "Destruktor A" << endl;}
};
class B : public A{
    public:
    B(int n) : A() {
        cout << "Konstruktor B - stworzenie tablicy " << n << " elementów " << endl;
        ptab = new int[n];
    }
    virtual ~B() {
        cout << "Destruktor B - zniszczenie tablicy " << endl;
        delete [] ptab;
    }
    private:
    int* ptab;
};
Out[2]:

In [3]:
A* tab[2];
tab[0] = new B(2);
tab[1] = new B(5);

// Działamy z tab

delete tab[0];
delete tab[1];
Konstruktor A
Konstruktor B - stworzenie tablicy 2 elementów 
Konstruktor A
Konstruktor B - stworzenie tablicy 5 elementów 
Destruktor B - zniszczenie tablicy 
Destruktor A
Destruktor B - zniszczenie tablicy 
Destruktor A
Out[3]:
(void) @0x7f5a267faba8

Zauważamy, że destruktory zostały wywołane poprawnie.

Uniemożliwienie dziedziczenia

Może zajść potrzeba uniemozliwienia dziedziczenia po danej klasie. Klasyczne podejście opierało się o przeniesienie konstruktora do sekcji private i stworzeniu statycznej metody tworzącej instancję obiektu. Takie rozwiązanie jest jednak niewygodne, wymaga stworzenia metody dla każdego typu konstruktora, jest nieczytelne i co najwazniejsze: o próbie dziedziczenia po takiej klasie kompilator poinformuje nas dopiero gdy spróbujemy stworzyć instancję klasy.

Na szczęcie standard C11 wprowadza slowo kluczowe final, które rozwiazuje ten problem.

Przykład przed C11:

In [1]:
class A {
private:
  A() {}
public:
  static A* zrobA(){return new A();}
};
class B : public A {};
Out[1]:

Bląd zobaczymy dopiero przy próbie utworzenia obiektu:

In [2]:
B b;
input_line_4:2:4: error: call to implicitly-deleted default constructor of 'B'
 B b;
   ^
input_line_3:11:11: note: default constructor of 'B' is implicitly deleted because base
      class 'A' has an inaccessible default constructor
class B : public A {};
          ^

Przykład po C11:

In [1]:
class A final {
public :
  A () {};
};
class B : public A {};
input_line_3:9:18: error: base 'A' is marked 'final'
class B : public A {};
                 ^
input_line_3:5:7: note: 'A' declared here
class A final {
      ^ ~~~~~

Metody i klasy abstrakcyjne

Wiemy już o metodach wirtualnych. Metody takie nie muszą definiować ciala w klasie bazowej, bo cialo takie może nie mieć sensu - np.: co mialo by oznaczać PoliczPole() w bazowej klasie ksztalt?

Metodę taka deklarujemy jako pustą poprzez =0 (patrz przykład). Jeżeli klasa posiada metodę abstrakcyjną staje się ona abstrakcyjna i nie ma możliwości utworzenia obiektu takiej klasy. (W innych językach klasa taka nazywa się też interfejsem). konieczne staje się dziedziczenie i implementacja wszystkich metod abstrakcyjnych.

Rozpatrzmy nastepujacy przykład. Klasa bazowa kształt jest abstrakcyjna i deklaruje dwie metody wirtualne pole() i obwod().

In [1]:
#include <iostream>
using namespace std;

class ksztalt {
public :
  virtual double pole() = 0;
  virtual double obwod() = 0;
  double p_o() {return pole()/obwod();}
};
Out[1]:

Klasa pochodna prostokąt implementuje obie metody:

In [2]:
class prostokat : public ksztalt {
public:
  prostokat(double a, double b) : a(a), b(b) {}
  virtual double pole(){return a*b;}
  virtual double obwod(){return 2*a+2*b;}
private:
  double a;
  double b;
};
Out[2]:

Spróbujmy teraz utworzyć instancję klas kształt i prostokąt:

In [3]:
ksztalt k;
input_line_5:2:10: error: variable type 'ksztalt' is an abstract class
 ksztalt k;
         ^
input_line_3:7:18: note: unimplemented pure virtual method 'pole' in 'ksztalt'
  virtual double pole() = 0;
                 ^
input_line_3:8:18: note: unimplemented pure virtual method 'obwod' in 'ksztalt'
  virtual double obwod() = 0;
                 ^

Nie możemy tego zrobić, ponieważ kształt jest klasą abstrakcyjną, natomiast:

In [4]:
prostokat p(4,5);
cout << p.pole() << " " << p.obwod() << " " << p.p_o() << endl;
20 18 1.11111
Out[4]:
(std::basic_ostream<char, std::char_traits<char> >::__ostream_type &) @0x7f64f3ce9e60

Lepsza farma

Możemy teraz spróbować zaprogramować farmę, wykorzystującą klasy i polimorfizm. Klasą bazową będzie klasa bydle i wykorzystywać będziemy wskaźnik polimorficzny bydle*. Hierarchia naszych klas będzie następująca:

Screenshot%20from%202019-11-01%2019-04-11.png

In [1]:
#include <iostream>
#include <vector>
using namespace std;

class bydle {
public :
  void jedz () {cout << " Amci amci" << endl;}
  virtual void dzwiek(){cout << "Not implemented for bydle" << endl;}
};

class kura : public bydle {
public :
  virtual void dzwiek() {cout << " Kokoko" << endl;}
};
class krowa : public bydle {
public :
  virtual void dzwiek() {cout << " Muuu" << endl;}
};
class osiol: public virtual bydle {
public :
  virtual void dzwiek() {cout << " Iooo" << endl;}
};
class kon : public virtual bydle {
public :
  virtual void dzwiek() {cout << " Ihaha" << endl;}
};
class mul: public kon, public osiol {
public :
  virtual void dzwiek() {cout << " ????" << endl;}
  // jaki dzwiek wydaje Mul?
};
Out[1]:

In [2]:
vector<bydle*> zagroda(10);
  
  zagroda[0]=new kura();
  zagroda[1]=new kura();
  zagroda[2]=new kura();
  zagroda[3]=new kura();
  
  zagroda[4]=new osiol();
  zagroda[5]=new osiol();
  zagroda[6]=new krowa();
  
  zagroda[7]=new kon();
  zagroda[8]=new mul();
  zagroda[9]=new mul();
  
  for(int i=0; i<zagroda.size(); ++i)
    zagroda[i]->jedz();
  for(int i=0; i<zagroda.size(); ++i)
    zagroda[i]->dzwiek();
  
  
  for(int i=0; i<zagroda.size(); ++i)
    delete zagroda[i];
 Amci amci
 Amci amci
 Amci amci
 Amci amci
 Amci amci
 Amci amci
 Amci amci
 Amci amci
 Amci amci
 Amci amci
 Kokoko
 Kokoko
 Kokoko
 Kokoko
 Iooo
 Iooo
 Muuu
 Ihaha
 ????
 ????
Out[2]:

In [ ]: