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

  Юрій  | 

  Оновл. 23 Лют 2021  | 

 203

На попередніх уроках ми вивчили основи спадкування в мові C++ і порядок ініціалізації дочірніх класів. На цьому уроці ми докладніше розглянемо роль конструкторів в ініціалізації дочірніх класів.

Конструктори і ініціалізація

А допомагати нам у цьому будуть класи Parent і Child:

Зі звичайними (НЕ дочірніми) класами конструктору потрібно морочитися тільки з членами свого класу. Наприклад, об’єкт класу Parent створюється наступним чином:

Ось що насправді відбувається при ініціалізації об’єкта parent:

   виділяється пам’ять об’єкту parent;

   викликається відповідний конструктор класу Parent;

   список ініціалізації ініціалізує змінні;

   виконується тіло конструктора;

   точка виконання повертається назад в caller.

Все досить-таки просто. З дочірніми класами справи йдуть трохи складніше:

Ось що відбувається при ініціалізації об’єкту child:

   виділяється пам’ять об’єкту дочірнього класу (достатня порція пам’яті для частини Parent і частини Child об’єкту класу Child);

   викликається відповідний конструктор класу Child;

   створюється об’єкт класу Parent з використанням відповідного конструктора класу Parent. Якщо такий конструктор програмістом не надано, то буде використовуватися конструктор за замовчуванням класу Parent;

   список ініціалізації ініціалізує змінні;

   виконується тіло конструктора класу Child;

   точка виконання повертається назад в caller.

Єдина відмінність між ініціалізацією об’єктів звичайного і дочірнього класів полягає в тому, що при ініціалізації об’єкта дочірнього класу, спочатку виконується конструктор батьківського класу (для ініціалізації частини батьківського класу) і тільки потім вже виконується конструктор дочірнього класу.

Ініціалізація членів батьківського класу

Одним з недоліків нашого дочірнього класу Child є те, що ми не можемо ініціалізувати m_id при створенні об’єкта класу Child. Що, якщо ми хочемо задати значення як для m_value (частини Child), так і для m_id (частини Parent)?

Початківці часто намагаються вирішити цю проблему наступним чином:

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

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

Чому C++ так робить? Відповідь пов’язана з константними змінними і посиланнями. Подумайте, що сталося б, якби 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.

Висновки

При ініціалізації об’єктів дочірнього класу, конструктор дочірнього класу відповідає за те, який конструктор батьківського класу викликати. Якщо цей конструктор явно не вказаний, то викликається конструктор за замовчуванням батьківського класу. Якщо ж компілятор не може знайти конструктор за замовчуванням батьківського класу (або цей конструктор не може бути створений автоматично), то компілятор видасть помилку.

Тест

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

Залишити відповідь

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