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

  Юрий  | 

    | 

  Обновл. 24 Июн 2019  | 

 4425

 ǀ   7 

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

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

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

Хотя это действительно сработает в этом случае, но это не сработает, если 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.

Заключение

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

Тест

Реализуем наш пример с Фруктом, о котором мы говорили в уроке №153. Создайте родительский класс 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 (26 оценок, среднее: 5,00 из 5)
Загрузка...

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

  1. Аватар Анатолий:

    Зачем слово friend перед перегрузкой оператора <<, она же внутри класса?

  2. Аватар Макс:

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

    1. Юрий Юрий:

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

      1. Аватар Макс:

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

        1. Юрий Юрий:

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

  3. Аватар Shom:

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

    1. Аватар korvell:

      Она таковой и является.

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

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