Віртуальні функції. Що це таке? Частина 1
Частина 1. Загальна теорія віртуальних функцій
Подивившись на назву цієї статті, ви можете подумати: "Хм! Хто ж не знає, що таке віртуальні функції! Це ж." Якщо це так, можете сміливо кинути читання прямо на цьому місці.
А для тих, хто тільки починає розбиратися в тонкощах С ++, але вже має, скажімо, початкові знання про таку річ, як успадкування. і щось чув про поліморфізм. має прямий сенс почитати цей матеріал. Якщо ви зрозумієте віртуальні функції, то отримаєте ключ до розгадки секретів успішного об'єктно-орієнтованого проектування.
Весь матеріал я вирішив розділити на 3 частини.
Давайте в першій частині спробуємо розібратися в загальній теорії віртуальних функцій. Подивимося в другій частині їх застосування (і їх міць і силу!) На будь-якому більш-менш життєвому прикладі. Ну, і в третій частині ще поговоримо про таку річ, як віртуальні деструктори.
Так що ж це таке?
Давайте для початку згадаємо, як в класичному програмуванні на С ви можете передати об'єкт даних в функцію. Нічого складного в цьому немає, треба тільки поставити тип переданого об'єкта в той час, коли ви пишете код функції. Тобто, щоб описати поведінку об'єктів, необхідно заздалегідь знати і описати їх тип. Сила ООП в цьому випадку проявляється в тому, що ви можете писати віртуальні функції так, щоб об'єкт сам визначав, яку функцію він повинен викликати, під час виконання програми.
Говорячи іншими словами - за допомогою віртуальних функцій об'єкт сам визначає свою поведінку (власні дії). Техніку використання віртуальних функцій якраз і називають поліморфізмом. Буквально поліморфізм означає володіння багатьма формами. Об'єкт у вашій програмі насправді може представляти не один клас, а безліч різних класів, якщо вони пов'язані механізмом успадкування із загальним базовим класом. Ну і поведінку об'єктів цих класів в ієрархії, звичайно ж буде різним.
Ну ось, а тепер до справи!
Як відомо, згідно з правилами С ++, покажчик на базовий клас може посилатися на об'єкт цього класу, а також на об'єкт будь-якого іншого класу, похідного від базового. Розуміння цього правила дуже важливо. Давайте розглянемо просту ієрархію деяких класів А, В і С. А буде у нас базовим класом, В - виводиться (породжується) з класу А, ну а З - виводиться з В. Пояснення дивіться на малюнку.
У програмі об'єкти цих класів можуть бути оголошені, наприклад, таким чином.
Згідно з цим правилом покажчик типу А може посилатися на будь-який з цих трьох об'єктів. Тобто ось це буде вірним:
А ось це вже не правильно:
Незважаючи на те, що покажчик point_to_Object має тип А *, а не З * (або В *), він може посилатися на об'єкти типу С (або В). Може бути правило буде більш зрозумілим, якщо ви будете думати про об'єкт С, як особливий вид об'єкта А. Ну, наприклад, пінгвін - це особливий різновид птахів, але він все таки залишається птахом, хоч і не літає. Звичайно, цей взаємозв'язок об'єктів і покажчиків працює тільки в одному напрямку. Об'єкт типу С - особливий вид об'єкта А, але ось об'єкт А не є особливим видом об'єкта С. Повертаючись до пінгвінів сміливо можна сказати, що якби всі птахи були особливим видом пінгвінів - вони б просто не вміли літати!
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.
Ну і що нам це дає?
Саме час подивитися - а що ж нам дають віртуальні функції? На теорію віртуальних функцій в загальних рисах ми поглянули. Пора розглянути якусь реальну ситуацію, де можна усвідомити практичне значення даного предмета в реальному світі програмування.
Класичний приклад (з мого досвіду - в 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
Створюючи на початку програми динамічні об'єкти-види зброї, будемо додавати покажчики на них в масив.
Для того щоб вказати, яку зброю знаходиться в користуванні, заведемо змінну-індекс масиву, значення якої будемо змінювати в залежності від обраного виду зброї.
В результаті цих зусиль, код, що описує застосування зброї в грі може виглядати, наприклад, так:
if (LeftMouseClick) Arms [TypeOfWeapon] -gt; Use1 ();
else Arms [TypeOfWeapon] -> Use2 ();
Усе! Ми створили код, який описує стрілянину-пальбу-війну ще до того, як вирішили, які типи зброї будуть використовуватися. Більш того. У нас взагалі ще немає жодного реального типу озброєння! Додаткова (іноді дуже важлива) вигода - цей код можна буде скомпілювати окремо і зберігати в бібліотеці. Надалі ви (або інший програміст) можете вивести нові класи з Weapon, зберегти їх в масиві Arms # 91;] і використовувати. При цьому не потрібно перекомпіляції вашого коду.
Особливо зауважте, що цей код не вимагає від вас точного завдання типів даних об'єктів на які посилаються покажчики Arms # 91;], потрібно тільки, щоб вони були похідними від Weapon. Об'єкти визначають під час виконання. яку функцію Use () їм слід викликати.
Деякі тонкощі застосування
Давайте трохи часу приділимо проблеми заміщення віртуальних функцій.
Повернемося до початку - до нудним класів А, В і С. Клас С на даний момент стоїть у нас в самому низу ієрархії, в кінці лінії спадкоємства. У класі С точно також можна визначити замещающую віртуальну функцію. Причому застосовувати ключове слово virtual зовсім необов'язково, оскільки це кінцевий клас в лінії успадкування. Функція і так буде працювати і вибиратися як віртуальна. Але! А ось якщо вам закортить вивести якийсь клас D з класу С, та ще й змінити поведінку функції v_function (), то тут якраз нічого і не вийде. Для цього в класі С функція v_function () повинна бути оголошена, як virtual. Звідси правило, яке можна сформулювати так: "один раз віртуальний - завжди віртуальний!". Тобто, ключове слово virtual краще не відкидати - раптом знадобиться?
Ще одна тонкість. У похідному класі можна визначати функцію з тим же ім'ям і з тим же набором параметрів, але з іншим типом значення, що повертається, ніж у віртуальній функції базового класу. В цьому випадку компілятор вилається на етапі компіляції програми.
Далі. Якщо в похідному класі ввести функцію з тим же ім'ям і типом значення, що повертається, що і віртуальна функція базового класу, але з іншим набором параметрів, то ця функція похідного класу вже не буде віртуальною. Навіть якщо ви Супроводьте її ключовим словом virtual, вона не буде тим, що ви очікували. В цьому випадку за допомогою покажчика на базовий клас при будь-якому значенні цього покажчика буде виконуватися звернення до функції базового класу. Згадайте правило про перевантаження функцій! Це просто різні функції. У вас вийде зовсім інша віртуальна функція. Взагалі кажучи подібні помилки досить важковловимий, оскільки обидві форми запису цілком припустимі й сподіватися на діагностику компілятора в цьому випадку не доводиться.
Звідси ще одне правило. При заміщенні віртуальних функцій потрібно повний збіг типів параметрів, імен функцій і типів значень, що повертаються в базовому і похідному класах.
І ще. Віртуальної функцією може бути тільки нестатичних компонентна функція класу. Віртуальної не може бути глобальна функція. Віртуальна функція може бути оголошена дружній (
) В іншому класі. Але про дружні функції ми поговоримо як ні будь в іншій статті.
Ось, власне, і все на цей раз.
В наступній частині ви побачите повністю функціональний приклад найпростішої програми, яка демонструє всі ті моменти, про які ми говорили.
При написанні цього матеріалу використовувалися такі книги:
Якщо у вас є питання - пишіть, будемо розбиратися.
Сергій Малишев (aka Михалич).