Віртуальні функції. Що це таке? Частина 3 (исходники), Різне, Програмування, статті

Частина 1 :: Частина 2


Віртуальні деструктори


Цією частиною ми завершимо розпочату в перший і друга частинах статті розгляд використання віртуальних функцій. Якщо ви читали серію статей “Класи. Копіювання і присвоювання”, то пам’ятаєте, що там йшлося про те, як створюються об’єкти класів. Тепер поговоримо про те, як вони знищуються. Правда, слід обмовитися відразу. Дуже непогано якщо ви вже знаєте, що таке деструктори в цілому. Оскільки ця теоретична частина сюди ніяк не вписується. Якщо ви не знайомі з цим, зовсім не завадить для початку почитати щось про конструкторів і деструкції класів.

Коли потрібні віртуальні деструктори?


Давайте почнемо вивчення питання з розгляду простого (і став вже класичним) прикладу.

Спорудимо якийсь клас, який може запам’ятовувати строкове значення. І нехай він у нас буде базовим класом (правда не абстрактним, так як це не важливо в даному випадку), з нього ми будемо виводити інші.
Отже.


Код:

class Base
{
  private:
    char *sp1;
  public:
Base (const char * S) {sp1 = strdup (S);} / / конструктор
~ Base () {delete sp1;} / / деструктор
};

Все просто. Конструктор класу виділяє пам’ять для рядка шляхом звернення до бібліотечної функції strdup і зберігає адресу нового рядка в покажчику sp1. Деструктор класу звільняє цю пам’ять, коли об’єкт класу Base виходить з області видимості.
Далі, з базового класу виведемо новий клас. Ось такий:

Код:

class Derived: public Base
{
  private:
    char *sp2;
  public:
    Derived(const char *S1, const char *S2): Base(S1)
{Sp2 = strdup (S2);} / / це був конструктор
~ Derived () {delete sp2;} / / а це вже деструктор
};

Цей клас зберігає другий рядок, на яку посилається його покажчик sp2. Новий конструктор викликає конструктор базового класу, передаючи рядок в базовий клас, а також виділяє пам’ять під другий рядок і зберігає адресу нового рядка в покажчику sp2. Деструктор цього класу звільняє цю пам’ять.
Тепер десь в програмі ми можемо створити об’єкт такого класу:

Код:

Derived MyStrings(string 1, string 2);

Коли цей об’єкт вийде з області видимості, спочатку викличеться деструктор класу Derived, а потім деструктор базового класу Base. Вся пам’ять буде акуратно звільнена. Все по теорії, все красиво.

Розглянемо інший варіант. Припустимо, що ми оголосили покажчик на базовий клас Base, але привласнили йому адресу об’єкту класу Derived. Це цілком допустимо, ми вже обговорювали це питання в попередніх частинах. Тобто, це буде виглядати в програмі так:

Код:

Base * pBase; / / покажчик на базовий клас
pBase=new Derived(string 1, string 2);

Що ж станеться, коли в програмі буде видалений об’єкт, на який посилається вказівник pBase?

Код:

delete pBase; //?????????????

Компілятор бачить, що покажчик pBase повинен посилатися на об’єкти класу Base (звідки б йому дізнатися, що саме присвоєно цьому покажчику?). І цілком природно програма викличе тільки деструктор базового класу, і він видалить один рядок, але залишить у пам’яті іншу. Адже деструктор класу Derived не викликався! Виходить класична витік пам’яті!

Ось коли потрібен віртуальний деструктор!
Все, що потрібно зробити для виправлення цієї ситуації це оголосити в класах деструктори з ключовим словом virtual. Таким чином, деструктори будуть виглядати так (решта опис класів для простоти не наведено):

Код:

virtual ~ Base () {delete sp1;} / / деструктор
virtual ~ Derived () {delete sp2;} / / деструктор

Що ж станеться в цьому випадку? Оскільки деструктори оголошені віртуальними, то їх виклики будуть компонуватися вже під час виконання програми. Тобто, об’єкти самі будуть визначати, який деструктор потрібно викликати. Оскільки наш покажчик pBase насправді посилається на об’єкт класу Derived, то деструктор цього класу буде викликаний, так само як і деструктор базового класу. Деструктор базового класу автоматично виконується після деструктора похідного класу.

Ну і, мабуть, останнє.
Може так статися, що в деяких випадках буде дуже зручно визначити в класі чистий віртуальний деструктор. Пам’ятайте чисті віртуальні функції? Вони дають нам абстрактні класи, від яких неможливо створити об’єкт. Це основа для побудови ієрархії класів. Однак, іноді зустрічаються класи, які мало б сенс зробити абстрактними, але для цього у вашому розпорядженні може не виявитися чистих віртуальних функцій. Як бути? Рішення не складне. Об’єднаємо поняття чистої віртуальної функції і віртуального деструктора. Треба просто оголосити в класі, який повинен бути абстрактним, чистий віртуальний деструктор.
Наведемо приклад. Припустимо, це буде клас, що описує щось незрозуміле.

Код:

class Something / / абстрактний клас без віртуальних функцій
{
   public:
virtual ~ Something () = 0; / / а це чистий віртуальний деструктор
};

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

Код:

Something::~Something() {};

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

На завершення сказаного парочка порад:
Якщо у класу є віртуальні функції, має прямий сенс створити для нього віртуальний деструктор. Навіть якщо він і не потрібен цього класу. Класи, які будуть потім зроблені від нього, може бути будуть містити деструктори, які повинні викликатися відповідним чином. Якщо ж клас не містить віртуальних функцій, то швидше за все він не передбачається до використання в якості базового. В такому випадку визначення в ньому віртуального деструктора зазвичай невиправдано.
І ще. Що називається з розбігу можна зробити дуже цікаву помилку. Просто за аналогією з деструктором оголосити конструктор віртуальним. Конструктори не можуть бути віртуальними. Будьте пильні!

Ось, власне, і все, що я хотів вам сказати про віртуальні функціях.


Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*