20.2 Konstruktory i destruktory klas pochodnych

Jak wspomnieliśmy, konstruktory nie są dziedziczone. Jeśli w klasie pochodnej nie zdefiniowaliśmy konstruktora, to zostanie użyty konstruktor domyślny. Aby jednak powstał obiekt klasy pochodnej, musi być najpierw utworzony podobiekt klasy nadrzędnej wchodzący w jego skład. On również zostanie utworzony za pomocą konstruktora domyślnego, który zatem musi istnieć!

Podobiekt klasy bazowej jest tworzony jeszcze przed wykonaniem konstruktora klasy pochodnej.

Co w takim razie zrobić, aby do konstrukcji podobiektu klasy bazowej zawartego w tworzonym właśnie obiekcie klasy pochodnej użyć konstruktora innego niż domyślny? Wówczas musimy w klasie pochodnej zdefiniować konstruktor, a wywołanie właściwego konstruktora dla podobiektu klasy bazowej musi nastąpić poprzez listę inicjalizacyjną — wewnątrz konstruktora byłoby już za późno (patrz rozdział o konstruktorach ). Na liście tej umieszczamy jawne wywołanie konstruktora dla podobiektu. Tak więc, jeśli klasą bazową jest klasa  A i chcemy wywołać jej konstruktor, aby „zagospodarował” podobiekt tej klasy dziedziczony w klasie B, to na liście inicjalizacyjnej konstruktora klasy pochodnej  B umieszczamy wywołanie A(...), gdzie w miejsce kropek wstawiamy oczywiście argumenty dla wywoływanego konstruktora. Wywołuje się tylko konstruktory bezpośredniej klasy bazowej („ojca”, ale nie „dziadka”; oczywiście konstruktor ojca poprzez swoją listę inicjalizacyjną może wywołać konstruktor swojego ojca...).

Na przykład w poniższym fragmencie definiujemy klasę bazową Point. Nie ma ona w ogóle konstruktora domyślnego.


P161: pix.cpp     Wywołanie konstruktora klasy bazowej

      1.  struct Point {
      2.      int x;
      3.      int y;
      4.      Point(int x, int y)
      5.          : x(x), y(y)
      6.      { }
      7.  };
      8.  
      9.  struct Pixel: public Point {
     10.      int color;
     11.      Pixel(int x, int y, int color)
     12.          : Point(x,y), color(color)
     13.      { }
     14.  };

Klasa Pixel dziedziczy z klasy Point. Ponieważ klasa Point nie miała konstruktora domyślnego, w klasie Pixel musimy przynajmniej jeden konstruktor zdefiniować i na jego liście inicjalizacyjnej wywołać jawnie konstruktor klasy Point z odpowiednimi argumentami, co robimy w linii 12 powyższego fragmentu.

Zauważmy, że nie wolno na liście inicjalizacyjnej konstruktora klasy pochodnej wymieniać nazw pól z klasy bazowej. W powyższym przykładzie na liście inicjalizacyjnej konstruktora klasy Pixel nie można umieścić wyrażenia x(x), bo składowa x pochodzi z klasy bazowej. Wolno natomiast, za pomocą wyrażenia color(color), zainicjować składową color, bo jest ona zadeklarowana w klasie pochodnej Pixel, a nie było jej w klasie bazowej. Składowe dziedziczone z klasy bazowej mogą być więc zainicjowane, ale tylko za pomocą jawnego wywołania konstruktora klasy bazowej z listy inicjalizacyjnej konstruktora klasy pochodnej.

Pewien problem powstaje przy definiowaniu konstruktora kopiującego. Pisząc konstruktor kopiujący w klasie pochodnej, musimy zastanowić się, w jaki sposób skonstruowany będzie podobiekt klasy bazowej zawarty w tworzonym obiekcie klasy pochodnej. Jeśli z listy inicjalizacyjnej konstruktora kopiującego klasy pochodnej jawnie nie wywołamy konstruktora kopiującego klasy bazowej, to do utworzenia podobiektu klasy bazowej użyty będzie konstruktor domyślny klasy bazowej (który wobec tego musi istnieć).

Jeśli jednak chcemy, aby do utworzenia podobiektu użyty został inny niż domyślny konstruktor klasy bazowej, to musimy jawnie go wywołać z listy inicjalizacyjnej. Jakiego jednak argumentu użyć przy tym wywoływaniu? Odpowiedź jest prosta i wynika z tego, o czym mówiliśmy w poprzednim podrozdziale: ponieważ typem parametru konstruktora kopiującego klasy bazowej  A jest A& (lub, częściej, const A&), więc wystarczy „wysłać” referencję do tego obiektu klasy pochodnej B, który jest argumentem wywołania konstruktora z klasy B (czyli jest obiektem-wzorcem). Tak jak bowiem mówiliśmy, jeśli typem parametru funkcji jest wskaźnik lub referencja do obiektu klasy bazowej, to dopuszczalnym argumentem wywołania jest wartość wskaźnikowa lub referencyjna odnosząca się do obiektu klasy pochodnej.

W poniższym programie, dla uproszczenia, w ogóle nie ma pól wskaźnikowych, ale definiujemy konstruktory kopiujące (i domyślne):


P162: cop.cpp     Konstruktory kopiujące

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  class A {
      5.      int a;
      6.  public:
      7.      A(const A& aa) {
      8.          a = aa.a;
      9.          cout << "Copy-ctor A, a = " << a << endl;
     10.      }
     11.  
     12.      A(int aa = 0) {
     13.          a = aa;
     14.          cout << "Def-ctor  A, a = " << a << endl;
     15.      }
     16.  
     17.      void showA() { cout << "a = " << a; }
     18.  };
     19.  
     20.  class B: public A {
     21.      int b;
     22.  public:
     23.      B(const B& bb)
     24.          : A(bb)
     25.      {
     26.          b = bb.b;
     27.          cout << "Copy-ctor B, b = " << b << endl;
     28.      }
     29.  
     30.      B(int bb = 1)
     31.          : A(1)
     32.      {
     33.          b = bb;
     34.          cout << "Def-ctor  B, b = " << b << endl;
     35.      }
     36.  
     37.      void showB() {
     38.          showA();
     39.          cout << ", b = " << b << endl;
     40.      }
     41.  };
     42.  
     43.  int main() {
     44.      B b1(2);
     45.      b1.showB();
     46.  
     47.      B b2(b1);
     48.      b2.showB();
     49.  }

W liniach 23-28 definiujemy konstruktor kopiujący dla klasy pochodnej B. Na liście inicjalizacyjnej wywołujemy konstruktor z klasy A, a jako argumentu używamy obiektu klasy B. Ponieważ B jest klasą pochodną, więc takie wywołanie „pasuje” do konstruktora kopiującego klasy A. Wynikiem jest
    Def-ctor  A, a = 1
    Def-ctor  B, b = 2
    a = 1, b = 2
    Copy-ctor A, a = 1
    Copy-ctor B, b = 2
    a = 1, b = 2
Jawne wywołanie konstruktora kopiującego z linii 24 można usunąć. Wtedy, zgodnie z tym co mówiliśmy, do skonstruowania podobiektu klasy A zostanie użyty konstruktor domyślny tej klasy; po wykomentowaniu linii 24 wydruk programu będzie zatem
    Def-ctor  A, a = 1
    Def-ctor  B, b = 2
    a = 1, b = 2
    Def-ctor  A, a = 0
    Copy-ctor B, b = 2
    a = 0, b = 2
Patrząc na ten i poprzedni wydruk, widzimy, że

Konstruktory wywoływane są w kolejności „od góry”: najpierw dla podobiektów klas bazowych, potem dla danej klasy. Jeśli klasa dziedziczy z kilku klas, to konstruktory klas bazowych są wywoływane w kolejności ich wystąpienia na liście dziedziczenia.

Tak będzie również dla bardziej rozbudowanej hierarchii dziedziczenia. Jeśli, na przykład, klasa C dziedziczy z B, która z kolei dziedziczy z  A, to podczas tworzenia obiektu klasy C najpierw zostanie utworzony obiekt A, następnie obiekt klasy B zawierający jako podobiekt utworzony już obiekt A, a dopiero na końcu obiekt klasy C zawierający jako podobiekt utworzony obiekt klasy B.

Z kolei, w trakcie konstrukcji obiektów poszczególnych klas najpierw tworzone są obiekty będące składowymi klasy (przez wywołanie ich konstruktorów, jeśli są to składowe obiektowe). Tworzone one są w kolejności ich zadeklarowania. Potem dopiero wykonywane jest ciało samego konstruktora klasy.


Jak wspomnieliśmy, destruktory również nie są dziedziczone. Jeśli są zdefiniowane, to

destruktory wywoływane są w kolejności odwrotnej do konstruktorów.

A zatem, podczas niszczenia obiektu najpierw wywoływany jest jego destruktor (oczywiście, jeśli jest zdefiniowany). Następnie usuwane są obiekty będące składowymi tego obiektu nie odziedziczonymi z klas bazowych. Jeśli są to składowe obiektowe, to oczywiście nastąpi wywołanie dla nich destruktorów, jeśli były zdefiniowane. Następnie wywoływany jest destruktor dla podobiektu bezpośredniej klasy bazowej, potem usuwane są obiekty będące niedziedziczonymi składowymi tego podobiektu i tak dalej. Ilustruje to poniższy program:


P163: condes.cpp     Kolejność wywoływania konstruktorów i destruktorów

      1.  #include <iostream>
      2.  using namespace std;
      3.  
      4.  struct K {
      5.      char k;
      6.      K(char kk = 'k') {
      7.          k = kk;
      8.          cout << "Ctor K\n";
      9.      }
     10.  
     11.      ~K() {
     12.          cout << "Dtor K\n";
     13.      }
     14.  };
     15.  
     16.  struct A {
     17.      char a;
     18.      A(char aa = 'a') {
     19.          a = aa;
     20.          cout << "Ctor A\n";
     21.      }
     22.  
     23.      ~A() {
     24.          cout << "Dtor A\n";
     25.      }
     26.  };
     27.  
     28.  struct B: public A {
     29.      char b;
     30.      K    k;
     31.      B(char bb = 'b') : A(bb) {
     32.          b = bb;
     33.          cout << "Ctor B\n";
     34.      }
     35.  
     36.      ~B() {
     37.          cout << "Dtor B\n";
     38.      }
     39.  };
     40.  
     41.  struct C: public B {
     42.      char c;
     43.      C(char cc = 'c') : B(cc) {
     44.          c = cc;
     45.          cout << "Ctor C\n";
     46.      }
     47.  
     48.      ~C() {
     49.          cout << "Dtor C\n";
     50.      }
     51.  };
     52.  
     53.  int main() {
     54.      C c;
     55.  }

Mamy tu hierarchię dziedziczenia A B C (jak zwykle strzałka wskazuje od klasy pochodnej do klasy bazowej). Klasa B ma też pole obiektowe typu K. W funkcji main tworzymy obiekt klasy C, który po wyjściu z funkcji jest niszczony. A oto wydruk z tego programu:
    Ctor A
    Ctor K
    Ctor B
    Ctor C
    Dtor C
    Dtor B
    Dtor K
    Dtor A
Zgodnie z tym co mówiliśmy, najpierw tworzony jest podobiekt klasy  A, a co za tym idzie wywoływany jest konstruktor tej klasy. Następnie tworzony jest obiekt klasy B. Obiekt ten ma składową obiektową typu K. Widzimy, że najpierw konstruowana jest ta składowa, a co za tym idzie wywoływany jest konstruktor klasy K, a dopiero potem wywoływany jest konstruktor klasy B. Dopiero na samym końcu wywoływany jest konstruktor klasy C.

Kolejność wywoływania destruktorów jest dokładnie odwrotna. W szczególności podczas niszczenia podobiektu klasy  B najpierw wywoływany jest destruktor tej klasy, a dopiero potem niszczona jest jego składowa obiektowa klasy K.

T.R. Werner, 23 lutego 2019; 23:59