Лабораторна робота № 12 Тема: Віртуальні функції. Мета: Ознайомитись із віртуальними функціями Теоретичні відомості: Функції, оголошені з специфікатором virtual, називаються віртуальними функціями. Введення віртуальних функцій в оголошення базового класу (всього лише один специфікатор) має такі значні наслідки для методології об'єктно-орієнтованого програмування, що ми зайвий раз наведемо модифіковане оголошення класу A: class A ( public: virtual int Fun1 (int); ); Один додатковий специфікатор в оголошенні функції і більше ніяких (поки ніяких) змін в оголошеннях похідних класів. Як завжди, дуже проста функція main (). У ній ми визначаємо покажчик на об'єкт базового класу, налаштовуємо його на об'єкт похідного типу, після чого за вказівником ми викликаємо функцію Fun1 (): void main () ( A * pObj; A MyA; AB MyAB; pObj = &MyA; pObj-> Fun1 (1); AC MyAC; pObj = &MyAC; pObj-> Fun1 (1); ) Якщо б не специфікатор virtual, результат виконання вираження виклику pObj-> Fun1 (1); був би очевидний: як відомо, вибір функції визначається типом покажчика. Однак специфікатор virtual міняє всю справу. Тепер вибір функції визначається типом об'єкта, на який налаштовується покажчик базового класу. Якщо у похідному класі оголошується нестатична функція, яка має ім'я, тип значення, що повертається і список параметрів збігаються з аналогічними характеристиками віртуальної функції базового класу, то в результаті виконання вираження виклику викликається функція-член похідного класу. Одразу треба зауважити, що можливість виклику функції-члена похідного класу за вказівником на базовий клас не означає, що з'явилася можливість спостереження за об'єктом "зверху вниз" з покажчика на об'єкт базового класу. Невіртуальний функції-члени і дані, як і раніше недоступні. І в цьому можна дуже легко переконатися. Для цього досить спробувати зробити те, що ми вже один раз виконали - викликати невідому в базовому класі функцію-член похідного класу: / / pObj-> Fun2 (2); / / pObj-> AC:: Fun1 (2); Результат негативний. Покажчик, як і раніше, налаштований лише на базовий фрагмент об'єкта похідного класу. І все ж таки виклик функцій похідного класу можливий. Колись, в розділах, присвячених опису конструкторів, нами було розглянуто перелік регламентних дій, які виконуються конструктором в ході перетворення виділеного фрагмента пам'яті в об'єкт класу. Серед цих заходів згадувалася ініціалізація таблиць віртуальних функцій. Наявність цих самих таблиць віртуальних функцій можна спробувати виявити за допомогою операції sizeof. Звичайно, тут все залежить від конкретної реалізації, але, принаймні, у версії Borland C + + об'єкт-представник класу, що містить оголошення віртуальних функцій, займає більше пам'яті, ніж об'єкт аналогічного класу, у якому ті ж самі функції оголошені без специфікатор virtual. cout << "Розміри об'єкту:" <<sizeof (MyAC) << "..." <<endl; Так що об'єкт похідного класу набуває додатковий елемент - покажчик на таблицю віртуальних функцій. Схему такого об'єкта можна подати так (покажчик на таблицю ми позначимо ідентифікатором vptr, таблицю віртуальних функцій - ідентифікатором vtbl): MyAC:: = vptr A AC vtbl:: = & AC:: Fun1 На нашій нової схеми об'єкта покажчик на таблицю (масив з одного елемента) віртуальних функцій не випадково відділений від фрагмента об'єкта, що представляє базовий клас лише пунктирною лінією. Він знаходиться у полі зору цього фрагмента об'єкта. Завдяки доступності цього покажчика оператор виклику віртуальної функції Fun1 pObj-> Fun1 (1); можна подати так: (* (pObj-> vptr [0])) (pObj, 1); Тут тільки на перший погляд все заплутано і незрозуміло. Насправді, в цьому операторі немає ні одного не відомого нам вирази. Виклик функції-члена базового класу забезпечується за допомогою кваліфікованого імені. pObj-> A:: Fun1 (1); У цьому операторі ми відмовляємося від послуг таблиці віртуальних функцій. При цьому ми повідомляємо транслятору про намір викликати функцію-член базового класу. Механізм підтримки віртуальних функцій строгий і дуже жорстко регламентований. Покажчик на таблицю віртуальних функцій обов'язково включається в самий "верхній" базовий фрагмент об'єкта похідного класу. До таблиці покажчиків включаються адреси функцій-членів фрагмента самого "нижнього" рівня, що містить оголошення цієї функції. Ми в черговий раз модифікуємо оголошення класів A, AB і оголошуємо новий клас ABC. Модифікація класів A і AB зводиться до оголошення в них нових функцій-членів: class A ( public: virtual int Fun1 (int key); virtual int Fun2 (int key); ); ::::: int A:: Fun2 (int key) ( cout << "Fun2 (" <<key << ") from A" <<endl; return 0; ) class AB: public A ( public: int Fun1 (int key); int Fun2 (int key); ); ::::: int AB:: Fun2 (int key) ( cout << "Fun2 (" <<key << ") from AB" <<endl; return 0; ) Клас ABC є похідним від класу AB: class ABC: public AB ( public: int Fun1 (int key); ); int ABC:: Fun1 (int key) ( cout << "Fun1 (" <<key << ") from ABC" <<endl; return 0; ) У цей клас входить оголошення функції-члена Fun1, яка оголошується в непрямому базовому класі A як віртуальна функція. Крім того, цей клас успадковує від безпосередньої бази функцію-член Fun2. Ця функція також оголошується в базовому класі A як віртуальна. Ми оголошуємо об'єкт-представник класу ABC: ABC MyABC; Його схему можна представити таким чином: MyABC:: = vptr A AB ABC vtbl:: = & AB:: Fun2 & ABC:: Fun1 Таблиця віртуальних функцій зараз містить два елементи. Ми налаштовуємо покажчик на об'єкт базового класу на об'єкт MyABC, потім викликаємо функції-члени: pObj = &MyABC; pObj-> Fun1 (1); pObj-> Fun2 (2); У цьому випадку неможливо викликати функцію-член AB:: Fun1 (), оскільки її адресу не міститься в списку віртуальних функцій, а з верхнього рівня об'єкта MyABC, на який налаштований покажчик pObj, вона просто не видно. Таблиця віртуальних функцій будується конструктором у момент створення об'єкта відповідного об'єкта. Безумовно, транслятор забезпечує відповідне кодування конструктора. Але транслятор не в змозі визначити зміст таблиці віртуальних функцій для конкретного об'єкта. Це завдання часу виконання. Поки таблиця віртуальних функцій не буде побудована для конкретного об'єкта, відповідна функція-член похідного класу не зможе бути викликана. У цьому легко переконатися, після чергової модифікації оголошення класів. Програма невелика, тому має сенс привести її текст повністю. Не слід тішитися з приводу операції доступу до компонентів класу::. Обговорення пов'язаних з цією операцією проблем ще попереду. # include <iostream.h> class A ( public: virtual int Fun1 (int key); ); int A:: Fun1 (int key) ( cout << "Fun1 (" <<key << ") from A." <<Endl; return 0; ) class AB: public A ( public: AB () (Fun1 (125);); int Fun2 (int key); ); int AB:: Fun2 (int key) ( Fun1 (key * 5); cout << "Fun2 (" <<key << ") from AB." <<Endl; return 0; ) class ABC: public AB ( public: int Fun1 (int key); ); int ABC:: Fun1 (int key) ( cout << "Fun1 (" <<key << ") from ABC." <<Endl; return 0; ) void main () ( ABC MyABC; / / Викликається A:: Fun1 (). MyABC.Fun1 (1); / / Викликається ABC:: Fun1 (). MyABC.Fun2 (1); / / Викликаються AB:: Fun2 () і ABC:: Fun1 (). MyABC.A:: Fun1 (1); / / Викликається A:: Fun1 (). A * pObj = &MyABC; / / Визначаємо та налаштовуємо покажчик. cout <<"==========" <<endl; pObj-> Fun1 (2); / / Викликається ABC:: Fun1 (). / / pObj-> Fun2 (2); / / Ця функція через покажчик недоступна! pObj-> A:: Fun1 (2); / / Викликається A:: Fun1 (). ) Тепер у момент створення об'єкта MyABC ABC MyABC; з конструктора класу AB (а він викликається раніше конструктора класу ABC), буде викликана функція A:: Fun1 (). Ця функція є членом класу A. Об'єкт MyABC ще до кінця не сформований, таблиця віртуальних функцій ще не заповнена, про існування функції ABC:: Fun1 () ще нічого не відомо. Після того, як об'єкт MyABC буде остаточно сформований, таблиця віртуальних функцій заповниться, а покажчик pObj буде налаштований на об'єкт MyABC, виклик функції A:: Fun1 () через покажчик pObj буде можливий лише з використанням повного кваліфікованого імені цієї функції: pObj-> Fun1 (1); / / Це виклик функції ABC:: Fun1 ()! pObj-> A:: Fun1 (1); / / Очевидно, що це виклик функції A:: Fun1 ()! Зауважимо, що виклик функції-члена Fun1 безпосередньо з об'єкта MyABC призводить до аналогічного результату: MyABC.Fun1 (1); / / Виклик функції ABC:: Fun1 (). А спроба виклику невіртуальному функції AB:: Fun2 () через вказівник на об'єкт базового класу закінчується невдачею. У таблиці віртуальних функцій адреси цієї функції немає, а з верхнього рівня об'єкта "подивитися вниз" неможливо. / / pObj-> Fun2 (2); / / Так не можна! Результат виконання цієї програмки наочно демонструє специфіку використання віртуальних функцій. Усього кілька рядків ... Fun1 (125) from A. Fun1 (1) from ABC. Fun1 (5) from ABC. Fun2 (1) from AB. Fun1 (1) from A. ========== Fun1 (2) from ABC. Fun1 (2) from A. Один і той же покажчик в ході виконання програми може налаштовуватися на об'єкти-представники різних похідних класів. У результаті в буквальному сенсі один і той вираз виклику функції-члена забезпечує виконання зовсім різних функцій. Вперше ми стикаємося з так званої пізньої або відкладений зв'язування. Зауважимо, що специфікація virtual відноситься тільки до функцій. Віртуальних даних-членів не існує. Це означає, що не існує можливості звернутися до даних-членам об'єкта похідного класу за вказівником на об'єкт базового класу, налаштованому на об'єкт похідного класу. З іншого боку, очевидно, що якщо можна викликати заміщають функцію, то безпосередньо "через" цю функцію відкривається доступ до всіх функцій і даних-членам членам похідного класу і далі "знизу-вгору" до всіх непріватним функцій і даних-членам безпосередніх і непрямих базових класів. При цьому з функції стають доступні всі непріватние характеристики та функції базових класів. І ще один маленький приклад, який демонструє зміна поведінку об'єкта-представника похідного класу після того, як одна з функція базового класу стає віртуальним. # include <iostream.h> class A ( public: void funA () (xFun ();}; / * virtual * / void xFun () (cout << "this is void A:: xFun ();"<< endl;); ); class B: public A ( public: void xFun () (cout << "this is void B:: xFun ();"<< endl;); ); void main () ( B objB; objB.funA (); ) На початку специфікатор virtual а визначенні функції A:: xFun () закоментовані. Процес виконання програми полягає у визначенні об'єкта-представника objB похідного класу B і виклику для цього об'єкта функції-члена funA (). Ця функція успадковується з базового класу, вона одна і очевидно, що її ідентифікація не викликає у транслятора ніяких проблем. Ця функція належить базового класу, а це означає, що в момент її виклику, управління передається "на верхній рівень" об'єкта objB. На цьому ж рівні розташовується одна з функцій з ім'ям xFun (), і саме цієї функції передається управління в ході виконання вираження виклику в тілі функції funA (). Мало того, з функції funA () просто неможливо викликати іншу однойменну функцію. У момент розбору структури класу A транслятор взагалі не має жодного уявлення про структуру класу B. Функція xFun () - член класу B виявляється недосяжна з функції funA (). Але якщо розкоментувати специфікатор virtual у визначенні функції A:: xFun (), між двома однойменними функціями встановиться ставлення заміщення, а породження об'єкту objB буде супроводжуватися створенням таблиці віртуальних функцій, відповідно до якої буде викликатися заміщає функція член класу B. Тепер для виклику заміщаються функції необхідно використовувати її кваліфіковане ім'я: void A:: funA () ( xFun (); A:: xFun (); ) Хід роботи Використати віртуальні функції у наступних структурах класів: «Спорт клубу». «Бібліотеки». «Обувного магазину» «Булочної» «Мобільного магазину» Тощо…