Програмування С, С++теорія та практика (частина 2)
2.7.1 Віртуальні функції
Нагадаємо, що покажчику на базовий клас можна присвоїти значення адреси об’єкту будь-якого похідного класу (див. розділ 2.6.1."Механізм успадкування"). При цьому виклик методів через такий покажчик відбувається у відповідності до типу покажчика, а не до фактичного типу об’єкта, на який він посилається в конкретний момент. Продемонструємо це на прикладі, для чого знову ж таки повернемося до вже згадуваних класів у попередніх розділах про службовців та менеджерів - етрІоуее та тападег, та додамо до їх протоколів однойменні функції, що друкують власну класову інформацію:
сіазз етріоуее { сЬаг* пате; риЬііс: етріоуее* пехЬ;
VОІС ргіпЬ()
{ соиЬ <<"етріоуее ..." ; };
// ...
};
сіазз тападег : риЬііс етріоуее { риЬііс:
VОІС ргіпЬ(){
соиЬ<< " тападег ...";};
// ...
} ;
Тут слід відповісти на декілька запитань, що стосуються виклику однойменних функцій у головній програмі. Яким чином відбудеться виклик однойменної функції при активації об’єкта похідного класу? VОіС таіп()
{
етріоуее е, *ерЬг; тападег т, *трЬг;
е.ргіпЬ() ; // клас етріоуее ...
т.ргіпЬ() ; // клас тападег ...
трЬг=&т;
трЬг->ргіпЬ() ; // клас тападег ...
ерЬг=трЬг; / / маємо право!
ерЬг->ргіпЬ();// а тут ні - знову клас етріоуее (!?)
}
Все нібито закономірно та не викликає сумнівів, окрім останнього оператора. Коли ми звертаємося до функції похідного об’єкту, використовуючи покажчик на базовий клас, викликається функція базового класу! Цей процес носить назву раннього зв’язку "клас+метод", коли зв’язки з методами встановлюються жорстко на етапі компоновки програми. Щоб викликати метод класу тападег, слід застосувати явне перетворення типу покажчика:
((тападег*)ерЬг)->ргіпЬ();
// після перетворення - клас тападег
Але усувати такі "непорозуміння" можна й іншим, більш гнучким шляхом - оголосити ргіпі() віртуальною функцією. Вона відрізнятиметься від звичайної функції-елемента лише додаванням ключового слова уігіиаі:
-уігЬиаІ VОіС ргіпЬ () ;
У спрощеному розумінні віртуальна функція - це функція, виклик якої залежить від типу об’єкта. В традиційному розумінні ми спочатку "прив'язали" об’єкт даних до функції, тобто раніше зв’язок “об’єкт + метод” визначався б на етапі написання коду. Оголосивши функцію віртуальною, ми підключаємо механізм пізнього зв’язку, коли визначення конкретного посилання на метод відбуватиметься на етапі виконання програми в залежності від типу об’єкта, який викликав метод. Оскільки ми ведемо мову про об'єктно-орієнтоване програмування, у нас з’являється набагато ефективніша можливість писати віртуальні функції, щоб сам об’єкт міг визначити, яку саме функцію необхідно активізувати під час виконання програми.
Але перш, ніж дійсно зрозуміти віртуальність, слід більш детальніше зупинитися на одному з найважливіших принципів класів, пов’язаних відношенням успадкування. Згідно об'єктно-орієнтованої парадигми, покажчик на базовий клас може посилатися не лише на об’єкт свого класу, але й на об’єкт іншого класу, похідного від базового (про це вже згадувалося у попередньому розділі: оскільки менеджер є службовцем, покажчик на етріоуее може посилатися не тільки на об’єкт свого класу, але й на похідний об’єкт цього класу тападег). Цей принцип стає особливо важливим, коли в класах, пов’язаних відношенням успадкування, визначаються віртуальні функції. Знову повернемося до вже відомих нам класових протоколів, але вже з віртуальними функціями:
с1азз етр1оуее { риЬ1іс:
^г'Ьиа1 VОій. ргіп^() {
соиї <<"етр1оуее ..." ;};
};
с1азз тападег : риЬ1іс етр1оуее { риЬ1іс:
^г'Ьиа1 VОій. ргіп^() { сои'Ь<< " тападег ..."; } ;
};
VОій. таіп () {
етр1оуее *ер£г; ер£г=пем тападег; ер^г->ргіп^() ;
}
Ми оголосили у головній програмі покажчик на базовий клас *еріг та присвоїли йому адресу новоствореного об'єкту похідного класу у динамічній пам’яті (покажчик еріг може зберігати адресу об'єкту не лише типу етріоуее, але й тападег). Викличемо віртуальну функцію еріг->ргіпі(). Ніякого приведення типу вже не потрібно: гарантовано, що під час виконання програми цей оператор викличе підходящу віртуальну функцію того класу, на об'єкт якого в даний момент посилається еріг, а саме тападег::ргіпі().
Деякі моменти опису та використання віртуальних функцій можна перерахувати у такому порядку:
1. Якщо метод, визначений у базовому класі, як віртуальний, також визначається у похідному класі з тим же ім’ям та списком параметрів, тоді він автоматично є також віртуальним. Віртуальні методи успадковуються, тобто перевизначення їх у похідному класі необхідно лише тоді, коли необхідно задати відмінні дії при виконанні цього методу у даному класі. При цьому права доступу при перевизначенні змінити неможливо.
2. Зарезервоване ключове слово уігіиаі вказує компілятору на побудову таблиці віртуальних правил УМТ (уігіиаі теґкоії іаЬІе), що міститиме адреси таких функцій для даного класу. Кожний представник класу з віртуальною функцією містить покажчик УРТК (уігіиаі роіпіег) на його таблицю віртуальних методів УМТ.
3. На етапі компіляції у початок конструктора автоматично вставляється покажчик УРТК на таблицю віртуальних правил
УМТ .
4. Адреса деякої віртуальної функції має одне й те ж саме зміщення в таблицях УМТ кожного класу конкретної ієрархії.
5. При активації віртуальних методів згенерований код спочатку знаходить покажчик УРТК на таблицю УМТ, а потім з таблиці вибирає адресу віртуальної функції, та, насамкінець, проводить безпосередній виклик функції.
6. Віртуальний механізм працює лише за допомогою покажчиків (посилань) на об'єкти. Об’єкт, що містить віртуальні функції, та визначений через покажчик або посилання, носить назву поліморфного. У даному випадку поліморфізм полягає у тому, що за допомогою одного й того ж звертання до методу виконуються різні дії в залежності від типу, на який посилається покажчик у даний момент часу.
7. Віртуальна функція не може бути оголошеною як зіаііс, але може бути оголошена, як дружня.
8. У базових класах рекомендується використовувати віртуальний деструктор.
Отже, виклики віртуальних функцій-членів визначаються під час виконання програми (що носить назву пізнього або динамічного зв’язування) на відміну від звичайних елементів-функцій, коли зв’язок “об’єкт + метод” визначається на етапі компіляції (як раннє або статичне зв’язування). Термін "пізнє зв’язування" іноді замінюють терміном поліморфізм (від грецької - різноманітний).
В чому полягає практичне значення поліморфізму в реальному програмуванні? Чіткого правила, за яким слід оголошувати методи віртуальним немає. Можна тільки дати рекомендацію оголошувати віртуальними методи, якщо існує вірогідність їх перевизначення у похідних класах. З іншого боку, при проектуванні ієрархії не завжди можна передбачити, яким чином будуть розширюватися базові класи у майбутньому, особливо при проектуванні обєктно-орієнтованих бібліотек класів. Тут поліморфізм дійсно дуже важливий, оскільки механізм успадкування без нього майже не принесе користі. (До речі, мови, в яких поліморфізм не підтримується, наприклад Ада, взагалі носять назви об'єктних.).
Виклик віртуальної функції реалізується як непрямий виклик за таблицею віртуальних правил. Ця таблиця створюється компілятором під час компіляції, а зв’язок відбувається під час виконання.
Не існує "ідеальної" ієрархії класів для кожної конкретної програми. По мірі просування на шляху розробки може виявитися ситуація, що доведеться вводити нові класи, які докорінно змінять усю ієрархію. Нічого дивного тут немає, адже кожна ієрархія класів являє собою поєднання експериментальних досліджень та інтуїції, основаних на практиці.
12 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
