На предыдущем уроке мы узнали, что классы могут наследовать переменные-члены и методы от других классов. На этом уроке мы рассмотрим порядок событий, которые выполняются при инициализации объектов дочернего класса.
Во-первых, вот 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++ выполняет построение дочерних классов поэтапно, начиная с верхнего класса иерархии и заканчивая нижним классом иерархии. По мере построения каждого класса для выполнения инициализации вызывается соответствующий конструктор соответствующего класса.
На следующем уроке мы рассмотрим роль конструкторов в процессе построения дочерних классов.
Я как всегда с вопросом, если родительский класс создается первым. То как насчёт зависимости класа родителя и класса наследника? Например в класе родитель есть конструктор с 1 параметром при этом родитель сугубо виртуальный (т-е этот класс никак нельзя создать (именно обьект класса) ) мы создаем далее дочерний класс и в нем в конструкторе описываем параметр для родителя; И выходит создать родителя мы не можем ибо это виртуальный класс и дочерний создать не можем ибо без переопределения виртуала и без записи параметра для родителя он не будет работать.
Спасибо за хорошо поданный материал. В примеры, наверное, стоило бы еще деструкторы добавить. Так как, как уже упоминалось в уроках, то могут быть проблемы с динамическими объектами.
Хей, хочу спросить. Как ты переводишь данные уроки ? То есть с помощью переводчика или прям по-своему, по-особому (например вдалбливал каждое слово в словарь и запоминал). Я имею ввиду именно словарный запас, где ты его набирал и как, ведь это технический инглиш получается.
Привет. Сам перевод у меня состоит из трех этапов. Первый — это с помощью Переводчика, затем адаптация к русскому языку, его правилам. И затем уже финальная вычитка/редактура. Но огромную помощь действительно оказывает Переводчик — экономит время. Но бывает, что и переписываю абзацы с нуля. А технический English — то я учусь на программиста, поэтому эта тема знакома + Гугл в помощь.