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ć!
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.
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. };
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):
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. }
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 = 2Jawne 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 = 2Patrząc na ten i poprzedni wydruk, widzimy, że
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
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:
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. }
Ctor A Ctor K Ctor B Ctor C Dtor C Dtor B Dtor K Dtor AZgodnie 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, 21 lutego 2016; 20:17