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

Частина 1. Загальна теорія віртуальних функцій


Подивившись на назву цієї статті, ви можете подумати: “Хм! Хто ж не знає, що таке віртуальні функції! Це ж …” Якщо це так, можете сміливо кинути читання прямо на цьому місці.

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

Взагалі кажучи – матеріал не дуже складний. І все про що тут буде говоритися, безсумнівно, можна знайти в книгах. Проблема тільки в тому, що ви, мабуть, не знайдете повного викладу всієї проблеми в одній, або двох книгах. Для того щоб написати про віртуальні функції, мені довелося “простудіювати” 6 різних видань. І навіть у цьому випадку я зовсім не претендую на повноту викладу. У списку літератури я вказую тільки основні, ті, що наштовхнули мене на стиль викладу і зміст.

Весь матеріал я вирішив розділити на 3 частини.
Давайте в першій частині спробуємо розібратися в загальній теорії віртуальних функцій. Подивимося у другій частині їх застосування (і їх потужність і силу!) На будь-якому більш-менш життєвому прикладі. Ну, і в третій частини ще поговоримо про таку річ, як віртуальні деструктори.


Так що ж це таке?


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

Говорячи іншими словами – за допомогою віртуальних функцій об’єкт сам визначає свою поведінку (власні дії). Техніку використання віртуальних функцій якраз і називають поліморфізмом. Буквально поліморфізм означає володіння багатьма формами. Об’єкт у вашій програмі в дійсності може представляти не один клас, а безліч різних класів, якщо вони пов’язані механізмом успадкування із загальним базовим класом. Ну і поведінку об’єктів цих класів в ієрархії, звичайно ж буде різним.

Ну от, а тепер до справи!


Як відомо, згідно з правилами С + +, покажчик на базовий клас може посилатися на об’єкт цього класу, а також на об’єкт будь-якого іншого класу, похідного від базового. Розуміння цього правила дуже важливо. Давайте розглянемо просту ієрархію якихось класів А, В і С. А буде у нас базовим класом, В – виводиться (породжується) з класу А, ну а С – виводиться з В. Пояснення дивіться на малюнку.


У програмі об’єкти цих класів можуть бути оголошені, наприклад, таким чином.
Код:
A object_A; / / оголошення об’єкта типу А
B object_B; / / оголошення об’єкта типу В
C object_C; / / оголошення об’єкта типу С

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

A * point_to_Object; / / оголосимо покажчик на базовий клас
point_to_Object = & object_C; / / привласнимо вказівником адреса об’єкту З
point_to_Object = & object_B; / / привласнимо вказівником адреса об’єкту В

А ось це вже не правильно:


В * point_to_Object; / / оголосимо покажчик на похідний клас
point_to_Object = & object_А; / / не можна привласнити покажчику адресу базового об’єкту


Незважаючи на те, що покажчик point_to_Object має тип А *, а не С * (або В *), він може посилатися на об’єкти типу С (або В). Може бути правило буде більш зрозумілим, якщо ви будете думати про об’єкт С, як особливому вигляді об’єкта А. Ну, наприклад, пінгвін – це особливий різновид птахів, але він все таки залишається птахом, хоч і не літає. Звичайно, цей взаємозв’язок об’єктів і покажчиків працює тільки в одному напрямку. Об’єкт типу С – особливий вид об’єкта А, але ось об’єкт А не є особливим видом об’єкта С. Повертаючись до пінгвінів сміливо можна сказати, що якби всі птахи були особливим видом пінгвінів – вони б просто не вміли літати!

Цей принцип стає особливо важливим, коли в класах, пов’язаних спадкуванням визначаються віртуальні функції. Віртуальні функції мають точно такий же вигляд і програмуються так само, як і звичайні функції. Тільки їх оголошення проводиться за ключовим словом virtual. Наприклад, наш базовий клас А може оголосити віртуальну функцію v_function().

class A
{
  public:
virtual void v_function (void) ;/ / функція описує якесь поведінку класу А
};


Віртуальна функція може оголошуватися з параметрами, вона може повертати значення, як і будь-яка інша функція. У класі може оголошуватися стільки віртуальних функцій, скільки вам буде потрібно. І знаходиться вони можуть у будь-якій частині класу – закритою, відкритою або захищеною.


Якщо в класі В, породженому від класу А потрібно описати коку-то іншу поведінку, то можна оголосити віртуальну функцію, названу знову-таки v_function().

class B: public A
{
  public:
virtual void v_function (void) ;/ / замещающая функція описує якесь
/ / Нове поведінку класу В
};

Коли в класі, подібному В, визначається віртуальна функція, що має однакову назву з віртуальною функцією класу-предка, така функція називається замещающей. Віртуальна функція v_function () в В заміщає віртуальну функцію з тим же ім’ям в класі А. Насправді все трохи складніше і не зводиться до простого збігу імен. Але про це трохи пізніше, в розділі “Деякі тонкощі застосування”.
Ну, а тепер найважливіше!

Повернемося до покажчика point_to_Object типу А *, який посилається на об’єкт object_В типу В *. Давайте уважно подивимося на оператор, який викликає віртуальну функцію v_function () для об’єкта, на який вказує point_to_Object.


A * point_to_Object; / / оголосимо покажчик на базовий клас
point_to_Object = & object_B; / / привласнимо вказівником адреса об’єкту В
point_to_Object->; v_function (); / / викличемо функцію


Покажчик point_to_Object може зберігати адреса об’єкту типу А або В. Значить під час виконання цей оператор point_to_Object-gt; v_function (); викликає віртуальну функцію класу на об’єкт якого він в даний момент посилається. Якщо point_to_Object посилається на об’єкт типу А, викликається функція, що належить класу А. Якщо point_to_Object посилається на об’єкт типу В, викликається функція, що належить класу В. Отже, один і той же оператор викликає функцію класу адресується об’єкта. Це і є дія, що визначається під час виконання програми.


Ну і що нам це дає?


Саме час подивитися – а що ж нам дають віртуальні функції? На теорію віртуальних функцій в загальних рисах ми поглянули. Пора розглянути яку-небудь реальну ситуацію, де можна усвідомити практичне значення аналізованого предмета в реальному світі програмування.

Класичний приклад (з мого досвіду – в 90% всієї літератури з С + +), який наводять в цих цілях – написання графічної програми. Будується ієрархія класів, щось типу “точка-gt; лінія-gt; плоска фігура-gt; об’ємна фігура “. І розглядається віртуальна функція, скажімо, Draw (), яка малює все це … Нудно!

Давайте розглянемо менш академічний, але все ж графічний приклад. (Классіка! Куди від неї подітися?). Спробуємо розглянути гіпотетично принцип, який може бути закладений в комп’ютерну гру. І не просто в гру, а в основу будь-якого (не важливо 3D або 2D, крутого чи так собі) шутера. Стрілялки, простіше кажучи. Я не кровожерливий по життю, але, грішний, люблю іноді пострілятися!

Отже, ми задумали зробити крутий шутер. Що знадобитися в першу чергу? Звичайно ж зброю! (Ну, нехай не в першу. Не важливо.) Залежно від того, на яку тему будемо складати, така зброя і знадобиться. Може це буде набір від простої дубини до арбалета. Може від аркебуза до гранатомета. А може і зовсім від бластера до дезінтегратора. Скоро ми побачимо, що це-то якраз і не важливо.

Що ж, раз є така маса можливостей, треба завести базовий клас.


class Weapon
{
   public:
… / / Тут будуть дані-члени, якими може описуватися, наприклад, як
/ / Товщина дубини, так і кількість гранат у гранатометі
/ / Ця частина для нас не важлива


virtual void Use1 (void) ;/ / зазвичай – ліва кнопка миші
virtual void Use2 (void) ;/ / зазвичай – права кнопка миші

… / / Тут будуть ще якісь дані-члени і методи
};


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

Колекцію озброєння треба десь зберігати. Мабуть, найпростіше організувати для цього масив покажчиків типу Weapon *. Для простоти припустимо, що це глобальний масив Arms, на 10 видів зброї, і всі покажчики для початку ініціалізовані нулем.


Weapon * Arms [10]; / / масив покажчиків на об’єкти типу Weapon


Створюючи на початку програми динамічні об’єкти-види зброї, будемо додавати покажчики на них в масив.

Для того щоб вказати, яку зброю перебуває в користуванні, заведемо змінну-індекс масиву, значення якої будемо змінювати в залежності від обраного виду зброї.


int TypeOfWeapon;


В результаті цих зусиль, код, що описує застосування зброї в грі може виглядати, наприклад, так:


if(LeftMouseClick) Arms[TypeOfWeapon]-gt;Use1();
else Arms[TypeOfWeapon]->Use2();


Все! Ми створили код, який описує стрілянину-пальбу-війну ще до того, як вирішили, які типи зброї будуть використовуватися. Більше того. У нас взагалі ще немає жодного реального типу озброєння! Додаткова (Іноді дуже важлива) вигода – цей код можна буде скомпілювати окремо і зберігати в бібліотеці. Надалі ви (або інший програміст) можете вивести нові класи з Weapon, зберегти їх у масиві Arms [] і використовувати. При цьому не потрібно перекомпіляції вашого коду.

Особливо зауважте, що цей код не вимагає від вас точного завдання типів даних об’єктів на які посилаються покажчики Arms [], потрібно тільки, щоб вони були похідними від Weapon. Об’єкти визначають під час виконання, яку функцію Use () їм слід викликати.


Деякі тонкощі застосування


Давайте трохи часу приділимо проблеми заміщення віртуальних функцій.

Повернемося до початку – до нудним класів А, В і С. Клас С на даний момент стоїть у нас в самому низу ієрархії, в кінці лінії успадкування. У класі С точно також можна визначити замещающую віртуальну функцію. Причому застосовувати ключове слово virtual зовсім необов’язково, оскільки це кінцевий клас в лінії спадкоємства. Функція і так працюватиме і вибиратися як віртуальна. Але! А от якщо вам закортить вивести якийсь клас D з класу С, та ще й змінити поведінку функції v_function (), то тут якраз нічого і не вийде. Для цього в класі С функція v_function () повинна бути оголошена, як virtual. Звідси правило, яке можна сформулювати так: “раз віртуальний – завжди віртуальний!”. Тобто, ключове слово virtual краще не відкидати – раптом знадобиться?

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

Далі. Якщо в похідному класі ввести функцію з тим же ім’ям і типом значення, що повертається, що і віртуальна функція базового класу, але з іншим набором параметрів, то ця функція похідного класу вже не буде віртуальною. Навіть якщо ви Супроводьте її ключовим словом virtual, вона не буде тим, що ви очікували. В цьому випадку за допомогою покажчика на базовий клас при будь-якому значенні цього покажчика буде виконуватися звернення до функції базового класу. Згадайте правило про перевантаження функцій! Це просто різні функції. У вас вийде зовсім інша віртуальна функція. Взагалі кажучи подібні помилки досить важковловимий, оскільки обидві форми запису цілком припустимі і сподіватися на діагностику компілятора в цьому випадку не доводиться.

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

І ще. Віртуальної функцією може бути тільки нестатичні компонентна функція класу. Віртуальної не може бути глобальна функція. Віртуальна функція може бути оголошена дружній (friend) в іншому класі. Але про дружні функціях ми поговоримо як-небудь в іншій статті.

Ось, власне, і все на цей раз.

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

Якщо у вас є питання – пишіть, будемо розбиратися.


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


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

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

Ваш отзыв

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

*

*