Лабораторна робота № 14 Тема: Абстрактні класи. Віртуальні деструктори. Мета: Розглянути абстрактні класи та віртуальні деструктори. Теоретичні відомості: Багато класів схожі з класом employee тим, що в них можна дати розумне визначення віртуальних функцій. Однак, є й інші класи. Деякі, наприклад, клас shape, представляють абстрактне поняття (фігура), для якого не можна створити об'єкти. Клас shape набуває сенс тільки як базовий клас в деякому похідному класі. Причиною є те, що неможливо дати осмислене визначення віртуальних функцій класу shape: class shape ( / / ... public: virtual void rotate (int) (error ( "shape:: rotate");) virtual void draw () (error ( "shape:: draw"):) / / Не можу ні обертати, ні малювати абстрактну фігуру / / ... ); Створення об'єкта типу shape (абстрактної фігури) законна, хоча абсолютно безглузда операція: shape s; / / нісенітниця: `` фігура взагалі'' Вона не має сенсу тому, що будь-яка операція з об'єктом s призведе до помилки. Краще віртуальні функції класу shape описати як чисто віртуальні. Зробити віртуальну функцію чисто віртуальної можна, додавши ініціалізатор = 0: class shape ( / / ... public: virtual void rotate (int) = 0; / / чисто віртуальна функція virtual void draw () = 0; / / чисто віртуальна функція ); Клас, в якому є віртуальні функції, називається абстрактним. Об'єкти такого класу створити не можна: shape s; / / помилка: мінлива абстрактного класу shape Абстрактний клас можна використовувати тільки як базовий для іншого класу: class circle: public shape ( int radius; public: void rotate (int) () / / нормально: / / Перевизначення shape:: rotate void draw (); / / нормально: / / Перевизначення shape:: draw circle (point p, int r); ); Якщо чисто віртуальна функція не визначається у похідному класі, то вона і залишається такою, а значить похідний клас теж є абстрактним. При такому підході можна реалізовувати класи поетапно: class X ( public: virtual void f () = 0; virtual void g () = 0; ); X b; / / помилка: опис об'єкта абстрактного класу X class Y: public X ( void f (); / / перевизначення X:: f ); Y b; / / помилка: опис об'єкта абстрактного класу Y class Z: public Y ( void g (); / / перевизначення X:: g ); Z c; / / нормально Абстрактні класи потрібні для завдання інтерфейсу без уточнення будь-яких конкретних деталей реалізації. Наприклад, в операційній системі деталі реалізації драйвера пристрою можна приховати таким абстрактним класом: class character_device ( public: virtual int open () = 0; virtual int close (const char *) = 0; virtual int read (const char *, int) = 0; virtual int write (const char *, int) = 0; virtual int ioctl (int ...) = 0; / / ... ); Справжні драйвери будуть визначатися як похідні від класу character_device. Після введення абстрактного класу у нас є всі основні засоби для того, щоб написати закінчену програму. Розглянемо маленький приклад: class A ( public: virtual void f () = 0; ~ A (); ); class B: public A ( public: virtual void f (); ~ B (); ); Виклик компілятора gcc рядком g + +-c-Wall test.cpp Дасть наступний результат: test.cpp: 6: warning: `class A 'has virtual functions but non-virtual destructor test.cpp: 13: warning: `class B 'has virtual functions but non-virtual destructor Це тільки попередження, компіляція пройшла цілком успішно. Тим не менше, чому ж gcc видає подібні попередження? Вся справа в тому, що віртуальні функції використовуються в C + + для обеспечення поліморфізму --- тобто, клієнтська функція виду: void call_f (A * a) ( a-> f (); ) ніколи не "знає" про те, що конкретно зробить виклик методу f () --- це залежить від того, якою насправді об'єкт представлений покажчиком a. Точно так само зберігаються покажчики на об'єкти: std:: vector <A*> a_collection; a_collection.push_back (new B ()); У результаті такого коду втрачається інформація про те, чим конкретно є кожен з елементів a_collection (я маю на увазі, без використання RTTI). У даному випадку це загрожує тим, що при видаленні об'єктів: for (std:: vector <A*>:: iterator i = ...) delete * i; всі об'єкти, що містяться в a_collection, будуть вилучені так, як ніби це --- об'єкти класу A. У цьому можна переконатися, якщо відповідним чином визначити деструктори класів A і B: inline A:: ~ A () ( puts ( "A:: ~ A ()"); ) inline B:: ~ B () ( puts ( "B:: ~ B ()"); ) Тоді виконання наступного коду: A * ptr = new B (); delete ptr; Чи призведе до наступного результату: A:: ~ A () Якщо ж у визначенні класу A деструктор був би зроблений віртуальним (virtual ~ A ();), то результат був би іншим: B:: ~ B () A:: ~ A () У принципі, все сказано. Але, незважаючи на це, дуже багато програмісти все одно не створюють віртуальних деструкторів. Одна з поширених помилок --- віртуальний деструктор потрібен лише в тому випадку, коли на деструктор породжених класів покладаються якісь нестандартні функції, якщо ж функціонально деструктор породженого класу нічим не відрізняється від деструктора предка, то робити його віртуальним зовсім необов'язково. Це неправда, тому що навіть якщо деструктор ніяких спеціальних дій не виконує, він все одно повинен бути віртуальним, інакше не будуть викликані деструктори для об'єктів-членів класу, які з'явилися по відношенню до предка. Тобто: # include <stdio.h> class A ( public: A (const char * n); ~ A (); protected: const char * name; ); inline A:: A (const char * n): name (n) () inline A:: ~ A () ( printf ( "A:: ~ A () for% s. \ n", name); ) class B ( public: virtual void f (); B (); ~ B (); protected: A a1; ); inline B:: ~ B () () inline B:: B (): a1 ( "a1") () void B:: f () () class C: public B ( public: C (); protected: A a2; ); inline C:: C (): a2 ( "a2") () int main () ( B * ptr = new C (); delete ptr; return 0; ) Компіляція цього прикладу проходить без помилок (але з попередженнями), висновок програми наступний: A:: ~ A () for a1. Трохи не те, що очікувалося? Тоді поставимо перед назвою деструктора класу B слово virtual. Результат зміниться: A:: ~ A () for a2. A:: ~ A () for a1. Тепер висновок програми кілька більш відповідає дійсності. Хід роботи Додати абстрактні класи в наступні структури: «Спорт клубу». «Бібліотеки». «Обувного магазину» «Булочної» «Мобільного магазину» Тощо…