Урок №176. Чисті віртуальні функції, Інтерфейси та Абстрактні класи

  Юрій  | 

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

 124

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

Абстрактні функції і класи

До цього моменту ми записували визначення всіх наших віртуальних функцій. Однак C++ дозволяє створювати особливий вид віртуальних функцій, так званих чистих віртуальних функцій (або «абстрактних функцій»), які взагалі не мають визначення! Перевизначають їх дочірні класи.

При створенні чистої віртуальної функції, замість визначення (написання тіла) віртуальної функції, ми просто присвоюємо їй значення 0.

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

Використання чистої віртуальної функції має два основних наслідки. По-перше, будь-який клас з однією і більше чистими віртуальними функціями стає абстрактним класом, об’єкти якого створювати заборонено! Подумайте, що станеться, якщо ми створимо об’єкт класу Parent:

Оскільки ми не визначали метод getValue(), то який результат виконання parent.getValue()?

По-друге, всі дочірні класи абстрактного батьківського класу повинні перевизначати всі чисті віртуальні функції, в іншому випадку — вони також будуть вважатися абстрактними класами.

Приклад чистої віртуальної функції

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

Ми заборонили створювати об’єкти класу Animal, зробивши конструктор protected. Однак, залишаються дві проблеми:

   Конструктор як і раніше доступний дочірнім класам, що дозволяє створювати об’єкти класу Animal.

   Як і раніше можуть бути дочірні класи, які не перевизначають метод speak().

Наприклад:

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

John says ???

Що сталося? Ми забули перевизначити метод speak(), тому lion.Speak() викликав Animal.speak() і отримали те, що отримали.

Рішення — використати чисту віртуальну функцію:

Тут є кілька речей, на які слід звернути увагу. По-перше, speak() тепер є чистою віртуальною функцією. Це означає, що Animal тепер абстрактний батьківський клас, і нам вже не потрібен специфікатор protected (хоча він і не буде зайвим). По-друге, оскільки наш клас Lion є дочірнім класу Animal, але ми не визначили Lion::speak(), то Lion вважається також абстрактним класом. Тому, якщо ми спробуємо скомпілювати наступний код:

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

Тепер вже інше діло:

John says RAWRR!

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

Чисті віртуальні функції з визначеннями

Виявляється, ми можемо визначити чисті віртуальні функції:

В цьому випадку speak() як і раніше вважається чистою віртуальної функцією (хоча пізніше ми її визначили), а Animal як і раніше вважається абстрактним батьківським класом (і об’єкти цього класу не можуть бути створені). Будь-який клас, який наслідує клас Animal, повинен перевизначити метод speak() або він також буде вважатися абстрактним класом.

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

Це корисно, коли ви хочете, щоб дочірні класи мали можливість перевизначати віртуальну функцію або залишити її реалізацію за замовчуванням (яку надає батьківський клас). У випадку, якщо дочірній клас задоволений реалізацією за замовчуванням, він може просто викликати її напряму. Наприклад:

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

Barbara says buzz

Інтерфейси

Інтерфейс — це клас, який не має змінних-членів і всі методи якого є чистими віртуальними функціями! Інтерфейси ще називають «класами-інтерфейсами» або «інтерфейсними класами».

Інтерфейсні класи прийнято називати з I на початку, наприклад:

Будь-який клас, який наслідує IErrorLog, повинен надати свою реалізацію всіх 3-х методів класу IErrorLog. Ви можете створити дочірній клас з ім’ям FileErrorLog, де openLog() відкриває файл на диску, closeLog() — закриває файл, а writeError() — записує повідомлення в файл. Ви можете створити ще один дочірній клас з ім’ям ScreenErrorLog, де openLog() і closeLog() нічого не роблять, а writeError() виводить повідомлення у спливаючому вікні.

Тепер припустимо, що вам потрібно написати програму, яка використовує журнал помилок (лог-файл). Якщо ви будете писати класи FileErrorLog або ScreenErrorLog напряму, то це неефективно. Наприклад, наступна функція змушує всі об’єкти, які викликають mySqrt(), використовувати FileErrorLog, що може бути не завжди доречно:

Набагато кращим варіантом буде реалізація через IErrorLog:

Тепер користувач через передачу об’єктів може визначити самостійно, який клас слід викликати. Якщо він хоче, щоб помилка була записана у файлі, то він передасть в функцію mySqrt() об’єкт класу FileErrorLog. Якщо він хоче, щоб помилка виводилася на екран, то він передасть об’єкт класу ScreenErrorLog. Або, якщо він хоче зробити те, що ви не передбачили, наприклад, відправити комусь Email-ом повідомлення помилки, то він може створити новий дочірній клас EmailErrorLog, який наслідуватиме IErrorLog, і передаватиме об’єкт цього класу! Таким чином, реалізація через IErrorLog робить нашу функцію гнучкішою і незалежною.

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

Інтерфейси надзвичайно популярні, тому що вони прості у використанні, зручні в підтримці, і їх функціонал легко розширювати. Деякі мови, такі як Java і C#, навіть додали в свій синтаксис ключове слово interface, яке дозволяє програмістам напряму визначати інтерфейсний клас, не вказуючи явно, що всі методи є абстрактними.

Чисті віртуальні функції і віртуальна таблиця

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

Тест

Чим відрізняється абстрактний клас від інтерфейсу в мові C++?

Відповідь

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

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

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

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

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