Урок №171. Віртуальні функції і Поліморфізм

  Юрій  | 

  Оновл. 22 Бер 2021  | 

 57

На даному уроці ми розглянемо, що таке віртуальні функції та поліморфізм в С++.


Віртуальні функції і Поліморфізм

На попередньому уроці ми розглянули ряд прикладів, в яких використання вказівників або посилань батьківського класу спрощувало логіку і зменшувало кількість коду. Проте, ми стикалися з проблемою, коли батьківський вказівник або посилання викликали тільки батьківські методи, а не дочірні. Наприклад:

Результат:

rParent is a Parent

Оскільки rParent є посиланням класу Parent, то викликається Parent::getName(), хоча фактично ми посилаємося на частину Parent об’єкта child.

На цьому уроці ми розглянемо, як можна вирішити дану проблему за допомогою віртуальних функцій.

Віртуальна функція в мові С++ — це особливий тип функції, яка, при її виклику, виконує «найдочірніший» метод, який існує між батьківським і дочірніми класами. Ця властивість відома як поліморфізм. Дочірній метод викликається тоді, коли збігається сигнатура (ім’я, типи параметрів і чи є метод константним) і тип повернення дочірнього методу з сигнатурою і типом повернення методу батьківського класу. Такі методи називаються перевизначеннями (або “перевизначеними методами”).

Щоб зробити функцію віртуальною, потрібно просто вказати ключове слово virtual перед оголошенням функції. Наприклад:

Результат:

rParent is a Child

Оскільки rParent є посиланням на батьківську частину об’єкта child, то, звичайно, при обробці rParent.getName() викликався б Parent::getName(). Проте, оскільки Parent::getName() є віртуальною функцією, то компілятор розуміє, що потрібно подивитися, чи є перевизначення цього методу в дочірніх класах. І компілятор знаходить Child::getName()!

Розглянемо складніший приклад:

Як ви думаєте, який результат виконання цієї програми?

Розглянемо все по порядку:

   Спочатку створюється об’єкт c класу C.

   rParent — це посилання класу A, якому ми вказуємо посилатися на частину A об’єкта c.

   Потім викликається метод rParent.getName().

   Виклик rParent.GetName() призводить до виклику A::getName(). Однак, оскільки A::getName() є віртуальною функцією, то компілятор шукає «найдочірніший» метод між A і C. У цьому випадку — це C::getName().

Зверніть увагу, компілятор не викликатиме D::getName(), оскільки наш вихідний об’єкт був класу C, а не класу D, тому розглядаються методи тільки між класами A і C.

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

rParent is a C

Складніший приклад

Розглянемо клас Animal з попереднього уроку, додавши тестовий код:

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

Matros says ???
Barsik says ???

А тепер розглянемо той же клас, але зробивши метод speak() віртуальним:

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

Matros says Meow
Barsik says Woof

Спрацювало!

При обробці animal.speak(), компілятор бачить, що метод Animal::speak() є віртуальною функцією. Коли animal посилається на частину Animal об’єкта cat, то компілятор переглядає всі класи між Animal і Cat, щоб знайти найбільш дочірній метод speak(). І знаходить Cat::speak(). У випадку, коли animal посилається на частину Animal об’єкта dog, компілятор знаходить Dog::speak().

Зверніть увагу, ми не зробили Animal::GetName() віртуальною функцією. Це через те, що GetName() ніколи не перевизначається ні в одному з дочірніх класів, тому в цьому немає необхідності.

Аналогічно з наступним прикладом з масивом тварин:

Результат:

Matros says Meow
Barsik says Woof
Ivan says Meow
Tolik says Woof
Martun says Meow
Tyzik says Woof

Незважаючи на те, що ці два приклади використовують тільки класи Cat і Dog, будь-які інші дочірні класи також працюватимуть з нашою функцією report() і з масивом тварин без внесення додаткових модифікацій! Це, мабуть, найбільша перевага віртуальних функцій — можливість структурувати код таким чином, щоб нові дочірні класи автоматично працювали зі старим кодом без необхідності внесення змін з боку програміста!

Попередження: Сигнатура віртуального методу дочірнього класу повинна повністю відповідати сигнатурі віртуального методу батьківського класу. Якщо у дочірнього методу буде інший тип параметрів, ніж у батьківського, то викликатися цей метод не буде.

Використання ключового слова virtual

Якщо функція позначена як віртуальна, то всі відповідні перевизначення теж вважаються віртуальними, навіть якщо біля них явно не вказано ключове слова virtual. Однак, наявність ключового слова virtual біля методів дочірніх класів послужить корисним нагадуванням про те, що ці методи є віртуальними, а не звичайними. Отже, хорошою практикою є вказування ключового слова virtual біля перевизначень в дочірніх класах, навіть якщо це не є строго необхідним.

Типи повернення віртуальних функцій

Типи повернення віртуальної функції і її перевизначень повинні збігатися. Розглянемо наступний приклад:

В цьому випадку Child::getValue() не рахується підходящим перевизначенням для Parent::getValue(), тому що типи повернень різні (метод Child::getValue() вважається повністю окремою функцією).

Не викликайте віртуальні функції в тілі конструкторів або деструкторів

Ось ще одна пастка для початківців. Ви не повинні викликати віртуальні функції в тілі конструкторів або деструкторів. Чому?

Пам’ятаєте, що при створенні об’єкта класу Child спочатку створюється батьківська частина цього об’єкту, а потім вже дочірня? Якщо ви будете викликати віртуальну функцію з конструктора класу Parent при тому, що дочірня частина створюваного об’єкта ще не була створена, то викликати дочірній метод замість батьківського буде неможливо, тому що об’єкт child для роботи з методом класу Child ще не буде створений. У таких випадках в мові C++ буде викликатися батьківська версія методу.

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

Правило: Ніколи не викликайте віртуальні функції в тілі конструкторів або деструкторів.

Недолік віртуальних функцій

«Якщо все так добре з віртуальними функціями, то чому б не зробити всі методи віртуальними?» — запитаєте Ви. Відповідь: “Це неефективно!”. Обробка і виконання виклику віртуального методу займає більше часу, ніж обробка і виконання виклику звичайного методу. Крім того, компілятор також повинен виділяти один додатковий вказівник для кожного об’єкта класу, який має одну або кілька віртуальних функцій.

Тест

Який результат виконання наступних програм? Не потрібно запускати/виконувати наступний код, ви повинні визначити результат без допомоги своєї IDE.

a)

Відповідь a)

Результат:

B

rParent — це посилання класу A на об’єкт c. rParent.getName() викликає A::getName(), але, оскільки A::getName() є віртуальною функцією, викликатиметься “найдочірніший” метод між класами A і C. А це B::getName(), тому що в класі C методу getName() немає.

b)

Відповідь b)

Результат:

C

Все доволі просто, C::getName() — це “найдочірніший” метод між класами B і C.

c)

Відповідь c)

Результат:

A

Оскільки getName() класу A не є віртуальним методом, то при обробці rParent.getName() викликається A::getName().

d)

Відповідь d)

Результат:

C

Хоча B і C не є віртуальними функціями, але A::getName() є віртуальною функцією, а B::getName() і C::getName() є перевизначеннями. Отже, B::getName() і C::getName() вважаються неявно віртуальними, і тому виклик rParent.getName() викличе C::getName().

e)

Відповідь e)

Результат:

A

Це вже трохи складніше. rParent — це посилання класу A на об’єкт c, тому rParent.getName() викликає A::getName(). Але оскільки A::getName() є віртуальною функцією, то викликається найбільш дочірній метод між A і C — A::getName(). Оскільки B::getName() і С::getName() не є const, то вони не вважаються перевизначеннями!

f)

Відповідь f)

Результат:

А

Ще одне хитре завдання. При створенні об’єкта c, спочатку виконується побудова батьківської частини A. Для цього викликається конструктор A, який, у свою чергу, викликає віртуальну функцію getName(). Оскільки частини класів B і C ще не створені, то виконується A::getName().

Оцінити статтю:

1 Зірка2 Зірки3 Зірки4 Зірки5 Зірок (3 оцінок, середня: 5,00 з 5)
Loading...

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

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