Урок 156. Конструкторы и инициализация дочерних классов

   | 

   | 

 Обновлено 19 Авг 2018  | 

 661

 ǀ   5 

В предыдущих двух уроках мы изучили основы наследования в C++ и порядок инициализации производных классов. В этом уроке мы более подробно рассмотрим роль конструкторов в инициализации дочерних классов. А помогать нам в этом будут классы Parent и Child:

С обычными классами (не дочерними) конструктору нужно заморачиваться только с членами своего класса. Например, объект класса Parent создается следующим образом:

Вот что на самом деле происходит при инициализации объекта parent:

  выделяется память для объекта parent;

  вызывается соответствующий конструктор класса Parent;

  список инициализации инициализирует переменные;

  выполняется тело конструктора;

  точка выполнения возвращается обратно в caller.

Всё довольно просто. С дочерними классами дела обстоят несколько сложнее:

Вот что происходит при инициализации объекта child:

  выделяется память для объекта дочернего класса (достаточная порция памяти для части Parent и части Child объекта класса Child);



  вызывается соответствующий конструктор класса Child;

  создается объект класса Parent с использованием соответствующего конструктора Parent. Если такового конструктора программистом не предоставлено, то будет использоваться конструктор по умолчанию класса Parent;

  список инициализации инициализирует переменные;

  выполняется тело конструктора;

  точка выполнения возвращается обратно в caller.

Единственное отличие между инициализацией объектов обычного и дочернего класса заключается в том, что при инициализации объекта дочернего класса, сначала выполняется конструктор родительского класса (для инициализации части родительского класса) и только потом уже выполняется конструктор дочернего класса.

Инициализация членов родительского класса

Одним из недостатков нашего дочернего класса Child является то, что мы не можем инициализировать m_id при создании объекта класса Child. Что, если мы хотим задать значения как m_value (части Child), так и m_id (части Parent)?

Новички часто пытаются решить эту проблему следующим образом:

Это хорошая попытка и почти правильная идея. Нам определённо нужно добавить еще один параметр в наш конструктор, иначе C++ не будет понимать, каким значением мы хотим инициализировать m_id.

Однако C++ запрещает дочерним классам инициализировать наследуемые переменные-члены родительского класса в списке инициализации своего конструктора. Другими словами, значение переменной может быть задано только в списке инициализации конструктора, принадлежащего к тому же классу, что и переменная-член.

Почему C++ так делает? Ответ связан с переменными const и ссылками. Подумайте, что произошло, если бы m_id был бы const. Поскольку константы должны быть инициализированы значениями при создании, то конструктор родительского класса должен установить это значение при создании переменной-члена. Однако, только после выполнения конструкторов родительского класса, выполняется конструктор дочернего класса. Каждый дочерний класс имел бы тогда возможность инициализировать эту переменную, потенциально меняя её значение! Ограничивая инициализацию переменных конструктором класса, к которому принадлежат эти переменные, C++ гарантирует, что все переменные будут инициализированы только один раз.

Конечным результатом выполнения кода выше является ошибка, так как m_id унаследован от класса Parent, а только ненаследуемые переменные-члены могут быть изменены в списке инициализации конструктора класса Child.

Однако наследуемые переменные могут по-прежнему изменять свои значения в теле конструктора через операцию присваивания. Следовательно, новички часто пытаются делать следующее:

Хотя это действительно работает в этом случае, но это не сработает, если m_id будет константой или ссылкой (поскольку константы и ссылки должны быть инициализированы в списке инициализации конструктора). Это также неэффективно, так как m_id присваивают значение дважды: первый раз в списке инициализации конструктора класса Parent, а затем снова — в теле конструктора класса Child. И, наконец, что, если классу Parent необходим доступ к этому значению во время инициализации?

Итого, как правильно инициализировать m_id при создании объекта класса Child?

Во всех наших примерах, при создании объекта класса Child, вызывался конструктор по умолчанию класса Parent. Почему так? Потому что мы не указывали иначе!

К счастью, C++ дает нам возможность явно выбирать конструктор класса Parent для выполнения инициализации части Parent! Для этого нам необходимо просто добавить вызов нужного нам конструктора в списке инициализации конструктора производного класса:

Теперь при выполнении следующего кода:

Конструктор Parent(int) будет использоваться для инициализации m_id значением 7, а конструктор производного класса будет использоваться для инициализации m_value значением 1.5!

Итого, результат выполнения программы выше:

ID: 7
Value: 1.5

Рассмотрим более подробно, что происходит:

  выделяется память для объекта child;

  вызывается конструктор Child(double, int), где value = 1.5, а id = 7;

  компилятор смотрит, запрашиваем ли мы какой-то конкретный конструктор класса Parent. И видит, что запрашиваем! Поэтому вызывается Parent(int) с параметром id, которому до этого мы присвоили значение 7;

  список инициализации конструктора класса Parent задает m_id значение 7;

  выполняется тело конструктора класса Parent, которое ничего не делает;

  завершается выполнения конструктора класса Parent;

  список инициализации конструктора Child присваивает m_value значение 1.5;

  выполняется тело конструктора класса Child, которое ничего не делает;

  завершается выполнения конструктора класса Child.

Это может показаться несколько сложным, но на самом деле всё очень просто. Всё, что происходит — это вызов конструктором Child конкретного конструктора Parent для инициализации части Parent объекта класса Child. Поскольку m_id находится в части Parent, то только конструктор Parent может инициализировать это значение.

Обратите внимание, не важно, где в списке инициализации конструктора Child вызывается конструктор Parent — он всегда будет выполняться первым.

Теперь мы можем сделать наши члены private

Теперь, когда мы знаем о инициализации членов родительского класса, нет никакой необходимости сохранять наши переменные-члены открытыми. Мы сделаем их private, как и должно быть.

В качестве напоминания: Доступ к членам public открыт для всех. Доступ к членам private открыт только для членов класса, в котором эти члены находятся. Обратите внимание, это означает, что дочерние классы не могут напрямую обращаться к закрытым членам родительского класса! Дочерним классам нужно использовать геттеры и сеттеры для доступа к этим членам.

Например:

В коде выше мы делаем m_id и m_value закрытыми. Для их инициализации используются соответствующие конструкторы, а для доступа – открытые функции доступа (геттеры).

Результат выполнения программы выше:

ID: 7
Value: 1.5

Другой пример

Рассмотрим еще пару классов, с которыми мы работали ранее:

Как мы уже знаем, BasketballPlayer только инициализирует свои собственные члены и не указывает использование конкретного конструктора Human. Это означает, что каждый созданный объект BasketballPlayer будет использовать конструктор по умолчанию класса Human, который будет инициализировать переменную-член name пустым значением, а age — значением 0. Поскольку мы хотим назвать нашего BasketballPlayer и указать его возраст при его создании, то мы должны изменить этот конструктор, добавив необходимые параметры.

Вот наши обновленные классы с членами private и с вызовом конкретного конструктора Human:

Теперь мы можем создавать объекты класса BasketballPlayer следующим образом:

Результат выполнения программы выше:

Anton Ivanovuch
45
310

Как видите, всё инициализировано правильно.

Цепочки наследований

Классы в цепочке наследований работают аналогично:

В этом примере класс C наследует свойства класса B, который наследует свойства класса A. Что произойдет при создании объекта класса C?

  main() вызовет C(int, double, char);

  конструктор C вызовет B(int, double);

  конструктор B вызовет A(int);

  поскольку A не наследует ни от кого, то построение начнется именно с этого класса;

  построение A выполнено, выводится значение 7 и выполнение переходит в B;

  класс B построен, выводится значение 5.4 и выполнение переходит в C;

  класс C построен, выводится значение «D» и выполнение возвращается обратно в main();

  Финиш!

Таким образом, результат выполнения программы выше:

A: 7
B: 5.4
C: D

Стоит отметить, что конструкторы дочернего класса могут вызывать конструкторы только того родительского класса, от которого они напрямую наследуют. Следовательно, конструктор класса C не может напрямую вызвать или передать параметры в конструктор класса A. Конструктор C может вызвать только конструктор B (который уже, в свою очередь, вызывает конструктор A).

Деструкторы

При уничтожении производного класса, каждый деструктор вызывается в обратном порядке построения классов. В примере выше, при уничтожении объекта класса С, сначала вызывается деструктор C, затем деструктор B, а затем уже деструктор A.

Итого

При инициализации объектов дочернего класса, конструктор дочернего класса отвечает за то, какой конструктор родительского класса вызывать. Если этот конструктор явно не указан, то вызывается конструктор по умолчанию родительского класса. Если же компилятор не может найти конструктор по умолчанию родительского класса (или этот конструктор не может быть создан автоматически), то компилятор выдаст ошибку.

Тест

Реализуем наш пример с Фруктом, о котором мы говорили в введении в наследование. Создайте родительский класс Fruit, который имеет два закрытых члена: name (std::string) и color (std::string). Создайте класс Apple, который наследует свойства Fruit. У Apple должен быть дополнительный закрытый член: fiber (тип double). Создайте класс Banana, который также наследует класс Fruit. Banana не имеет дополнительных членов.

Следующий код:

должен производить следующий результат:

Apple(Red delicious, red, 7.3)
Banana(Cavendish, yellow)

Подсказка: Поскольку a и b являются const, то убедитесь, что ваши параметры и функции соответствуют const.

Ответ

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (6 оценок, среднее: 5,00 из 5)
Загрузка...
Подписаться на обновления:

Комментариев: 5

  1. Макс:

    А Вы сделали PDF — файл по всему курсу?

    1. Юрий Юрий:

      В скором времени будет pdf половины курса. Затем, как переведу все уроки, весь курс + пошаговое создание игры на С++.

      1. Макс:

        Спасибо. А где можно будет скачать файлы?

        1. Юрий Юрий:

          На этом же сайте, в отдельной странице. Всё будет 🙂

  2. Shom:

    На первый взгляд тестовая задача гораздо проще казалась. )

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

ПОДПИСЫВАЙТЕСЬ

НА КАНАЛ RAVESLI В TELEGRAM

@ravesli

ПОДПИСАТЬСЯ БЕСПЛАТНО