На попередньому уроці ми дізналися, що класи можуть успадковувати змінні-члени і методи від інших класів. На цьому уроці ми розглянемо порядок дій, які виконуються при ініціалізації об’єктів дочірнього класу.
По-перше, ось 2 класи, які допомагатимуть нам ілюструвати важливі моменти:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
class Parent { public: int m_id; Parent(int id=0) : m_id(id) { } int getId() const { return m_id; } }; class Child: public Parent { public: double m_value; Child(double value=0.0) : m_value(value) { } double getValue() const { return m_value; } }; |
У цьому прикладі клас Child є дочірнім, а клас Parent — батьківським:
Оскільки Child успадковує змінні-члени і методи класу Parent, то ви можете припустити, що члени класу Parent копіюються в клас Child, але це не так. Замість цього розглядайте Child як клас, який складається з двох частин: перша — Parent, друга — Child:
Ви вже бачили безліч прикладів того, що відбувається при створенні об’єктів звичайного (НЕ дочірнього) класу:
1 2 3 4 5 6 |
int main() { Parent parent; return 0; } |
Parent — це не дочірній клас, тому що він не наслідує властивості будь-яких інших класів. Мова C++ виділяє пам’ять для Parent, потім викликається конструктор за замовчуванням класу Parent для виконання ініціалізації.
Тепер розглянемо, що відбувається при створенні об’єктів дочірнього класу:
1 2 3 4 5 6 |
int main() { Child child; return 0; } |
На перший погляд все начебто так само, як і в попередньому прикладі, але “під капотом” це не зовсім так. Як ми вже говорили, клас Child складається з двох частин: частина Parent і частина Child. Коли C++ створює об’єкти дочірніх класів, то він робить це поетапно. Спочатку створюється самий верхній клас ієрархії (той, який батько). Потім створюється дочірній клас, який йде наступним по порядку, і так до тих пір, поки не буде створено останній клас (той, який знаходиться в самому низу ієрархії).
Тому, при створенні об’єкта класу Child, спочатку створюється частина Parent класу Child (з використанням конструктора за замовчуванням класу Parent) і після того, як з частиною Parent завершено, створюється друга частина Child (з використанням конструктора за замовчуванням класу Child). У нашому прикладі більше немає дочірніх класів, тому на цьому все і завершується.
Цей процес насправді легко проілюструвати:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
#include <iostream> class Parent { public: int m_id; Parent(int id=0) : m_id(id) { std::cout << "Parent\n"; } int getId() const { return m_id; } }; class Child: public Parent { public: double m_value; Child(double value=0.0) : m_value(value) { std::cout << "Child\n"; } double getValue() const { return m_value; } }; int main() { std::cout << "Instantiating Parent:\n"; Parent dParent; std::cout << "Instantiating Child:\n"; Child dChild; return 0; } |
Результат виконання програми:
Instantiating Parent:
Parent
Instantiating Child:
Parent
Child
Як ви можете бачити, при створенні Child спочатку створюється частина Parent класу Child. У цьому є сенс, тому що (логічно) дитина не може існувати без батьків. Це також сприяє безпеці та ефективності виконання коду: дочірній клас часто використовує змінні-члени і методи батька, але батьківський клас нічого не знає про свій дочірній клас. Початкова ініціалізація батьківського класу гарантує, що його змінні-члени і методи будуть проініціалізовані до моменту використання їх дочірнім класом.
Порядок побудови класів в “ланцюжку” спадкувань
Часто трапляються ситуації, коли одні класи успадковують властивості інших класів, які, в свою чергу, успадковують властивості своїх попередніх (батьківських) класів. Наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class A { public: A() { std::cout << "A\n"; } }; class B: public A { public: B() { std::cout << "B\n"; } }; class C: public B { public: C() { std::cout << "C\n"; } }; class D: public C { public: D() { std::cout << "D\n"; } }; |
Пам’ятайте, що в C++ завжди йде побудова з «першого» або «топового» класу ієрархії. Потім C++ переходить до наступного класу ієрархії і виконує його побудову. Цей процес послідовний.
Проілюструємо порядок побудови класів у вищенаведеному “ланцюжку” спадкування:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
int main() { std::cout << "Constructing A: \n"; A cA; std::cout << "Constructing B: \n"; B cB; std::cout << "Constructing C: \n"; C cC; std::cout << "Constructing D: \n"; D cD; } |
Результат:
Constructing A:
A
Constructing B:
A
B
Constructing C:
A
B
C
Constructing D:
A
B
C
D
Висновки
Мова C++ виконує побудову дочірніх класів поетапно, починаючи з верхнього класу ієрархії і закінчуючи нижнім класом ієрархії. У міру побудови кожного класу для виконання ініціалізації викликається відповідний конструктор відповідного класу.
На наступному уроці ми розглянемо роль конструкторів в процесі побудови дочірніх класів.