C++. Поліморфізм. Віртуальні функції. Загальні поняття

Поліморфізм. Віртуальні функції. Загальні поняття. Специфікатори virtual та override. Приклади


Зміст


Пошук на інших ресурсах:

1. Поліморфізм. Загальні поняття. Раннє та пізнє зв’язування. Виклик віртуальної функції в ієрархії класів

Віртуальні функції реалізують так званий поліморфізм. Термін “поліморфізм” походить від грецьких слів poly (багато) та morphos (форма). Поліморфізм – це властивість програмного коду змінювати свою поведінку в залежності від ситуації, яка виникає при виконанні програми. В контексті реалізації, поліморфізм – це технологія виклику віртуальних функцій, що реалізовані в ієрархічно зв’язаних класах. Ієрархія класів формується на основі механізму спадковості.

З поняттям поліморфізм тісно зв’язане поняття віртуальна функція. Це спеціальним чином оформлена функція, яка може бути в так званому поліморфному стані – стані, при якому виклик потрібної функції з набору віртуальних, формується на етапі пізнього зв’язування. Поняття пізнє зв’язування означає, що код виклику потрібної функції формується під час виконання програми. Іншими словами, у вихідному коді виклик функції тільки позначається без точної вказівки того, яка саме функція повинна бути викликана. Об’єкт, для якого викликається віртуальна функція, носить загальне значення. Конкретний об’єкт та відповідна йому функція будуть сформовані на етапі виконання програми.

Механізм віртуальних функцій реалізує основний принцип поліморфізму: “один інтерфейс, декілька реалізацій” або “один інтерфейс, декілька методів”.

Як відомо, існує також і раннє зв’язування. При ранньому зв’язуванні відомо, які об’єкти використовуються при виклику функції у кожному випадку. Це накладає певні обмеження на можливості програмного коду. Зміна об’єктів для функцій з однаковими іменами неможлива у процесі виконання програми, цю зміну потрібно програмувати кожного разу вручну (це і є обмеження коду).

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

Для реалізації пізнього зв’язування потрібно виконання наступних умов:

  • класи повинні утворювати ієрархію з допомогою механізму спадковості;
  • в ієрархії класів були визначені функції, які мають однакове ім’я та перелік параметрів;
  • функції з однаковим ім’ям та параметрами повинні бути позначені як віртуальні (з ключовим словом virtual).

Стосовно парадигми класів та об’єктів, для поліморфізму можна виділити наступні характерні особливості:

  • поліморфізм не характеризує об’єкт;
  • реалізацію поліморфізму визначає архітектура класу;
  • поліморфізм є характеристикою функцій-членів класу;
  • увесь клас не може бути поліморфним, поліморфними можуть бути тільки функції-члени класу.

 

2. Види поліморфізму. Динамічний поліморфізм. Віртуальна функція. Організація ланцюжка віртуальних функцій. Специфікатори virtual та override

У мові C++ є можливість реалізовувати два види поліморфізму:

  • статичний. Цей вид поліморфізму досягається шляхом використання перевантажених функцій (раннє зв’язування). Більш детально про перевантаження функцій можна прочитати тут;
  • динамічний. У цьому випадку використовується спадковість у поєднанні з віртуальними функціями (пізнє зв’язування).

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

Сигнатура віртуальної функції, яка оголошена в базовому класі, визначає вид інтерфейсу, який реалізує ця функція. Інтерфейс визначає спосіб виклику віртуальної функції. Для кожного конкретного класу віртуальна функція має власну реалізацію, яка забезпечує виконання дій, що властиві тільки цьому класу. Таким чином, віртуальна функція для конкретного класу є деяким унікальним (конкретним) методом (specific method).

У найбільш спрощеному вигляді оголошення віртуальної функції у класі може бути таким:

class BaseClass
{
  virtual return_type FuncNameVirtual(list_of_parameters)
  {
    // ...
  }
};

тут

  • FuncNameVirtual() – ім’я віртуальної функції;
  • return_type – тип, який повертає функція;
  • list_of_parameters – список параметрів, які отримує функція.

В успадкованому класі віртуальна функція FuncNameVirtual() продовжує ланцюжок віртуальних функцій для класів нижчих рівнів. Для цього не обов’язково вказувати ключове слово virtual, оскільки це слово вже вказане у базовому класі BaseClass.

class DerivedClass
{
  return_type FuncNameVirtual(list_of_parameters)
  {
    // Це також віртуальна функція, яка перевизначає функцію базового класу
    // ...
  }
}

Можливі ситуації, коли у похідному класі оголошена функція, яка може сприйматись як віртуальна, однак вона не є віртуальною. Приклади таких ситуацій:

  • функція з такою самою сигнатурою як у базовому класі, але оголошена як константна (const);
  • функція, якій передаються аргументи типу, що сумісний з аргументами функції базового класу;
  • у базовому класі не існує функції з іменем, яке оголошується як override у похідному класі.

Щоб остаточно вказати компілятору, що функція успадкованого класу DerivedClass перевизначає функцію базового класу BaseClass, потрібно після оголошення параметрів функції вказати специфікатор override за зразком нижче

class DerivedClass
{
  return_type FuncNameVirtual(list_of_parameters) override
  {
    // Для ланцюжка віртуальних функцій так потрібно робити завжди
    // ...
  }
}

Після вказання специфікатора override компілятор має інформацію про те, що функція FuncNameVirtual() похідного класу DerivedClass призначена для того, щоб перевизначати однойменну віртуальну функцію базового класу. Відповідно до цього, компілятор буде контролювати весь ланцюжок оголошень виду virtual override.

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

При використанні віртуальних функцій специфікатор override необхідний з наступних причин:

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

 

3. Випадки реалізації поліморфізму

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

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

  • при оголошенні покажчика (*) на базовий клас та виклику віртуальної функції відповідного екземпляру класу, що є частиною ієрархії. Як відомо, у цьому випадку, покажчик на базовий клас може бути встановлений в значення екземплярів похідних класів. Після цього викликається відповідна віртуальна функція;
  • при передачі покажчика (*) на базовий клас у деяку функцію, яка викликає віртуальну функцію базового класу з допомогою оператора -> (доступ за покажчиком);
  • при передачі посилання (&) на базовий клас у деяку функцію, яка викликає віртуальну функцію базового класу з допомогою оператора . ‘ (крапка, доступ за посиланням).

 

4. Приклади реалізації поліморфізму
4.1. Приклад поліморфізму для двох класів. Виклик віртуальної функції за покажчиком (->). Аналіз коду

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

#include <iostream>
using namespace std;

// Базовий клас
class Base
{
public:
  // virtual - ознака віртуальної функції
  virtual void PrintInfo()
  {
    cout << "Base." << endl;
  }
};

// Успадкований клас від класу Base,
// важливо: має бути модифікатор public
class Derived : public Base
{
public:
  virtual void PrintInfo() override // специфікатор override потрібно вказати
  {
    cout << "Derived." << endl;
  }
};

void main()
{
  // 1. Створити екземпляри базового та похідного класу
  Base obj1;
  Derived obj2;

  // 2. Оголосити покажчик на базовий клас
  Base* p;

  // 3. Використати правило: покажчик на базовий клас може вказувати
  //    на будь-який екземпляр базового та похідного класу від базового.
  //        Нижче демонструється поліморофізм.
  // 3.1. Встановити покажчик p на екземпляр базового класу obj1
  //     і викликати PrintInfo()
  p = &obj1;
  p->PrintInfo(); // Base

  // 3.2. Встановити покажчик p на екземпляр похідного класу
  //      і викликати PrintInfo()
  p = &obj2;
  p->PrintInfo(); // Derived - це є поліморфізм (слово virtual)
}

Результат виконання програми

Base.
Derived.

Проаналізуємо вищенаведений код.

У прикладі оголошується два класи Base та Derived, які утворюють ієрархію з допомогою механізму успадкування.

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

...

// 3. Оголосити покажчик на базовий клас
Base* p;

...

Тепер покажчику p можна присвоювати адресу будь-якого з екземплярів класів Base та Derived. Спочатку присвоюється адреса екземпляру obj1 типу Base та викликається метод PrintInfo()

p = &obj1;
p->PrintInfo(); // Base

Вивід буде прогнозований – слово “Base”.

Потім покажчику p присвоюється адреса екземпляру obj2 типу Base та також викликається метод PrintInfo()

p = &obj2;
p->PrintInfo();

Вивід буде “Derived”. Тобто, буде викликаний метод PrintInfo() похідного класу, що нам і потрібно. Виклик цього методу забезпечує ключове слово virtual в оголошенні функції PrintInfo() базового класу Base.

Якщо у класі Base перед оголошення функції PrintInfo() забрати ключове слово virtual, то в наступному коді

p = &obj2;
p->PrintInfo();

буде викликаний метод PrintInfo() класу Base, а не класу Derived. Це означатиме, що поліморфізм не буде підтримуватись а завжди буде викликатись функція базового класу. У результаті програма буде виводити

Base.
Base.

Таким чином, функція PrintInfo() класу Derived для покажчика p на базовий клас Base буде недоступна.

Висновок. Поліморфізм реалізує правило “один інтерфейс, багато реалізацій”. У нашому випадку інтерфейс один – це оголошення та виклик функції PrintInfo()

p->PrintInfo();

Але в залежності від того, на який об’єкт вказує покажчик p буде викликано відповідний метод PrintInfo() – це і є багато реалізацій.

 

4.2. Приклад, що пояснює поліморфізм для трьох класів. Передача у функцію покажчика (*) на базовий клас

Дається приклад 3-х класів CalcLength, CalcArea, CalcVolume, в яких у віртуальній функції Calc() повертається відповідно довжина кола, площа круга та об’єм кулі.

Для демонстрації створюється деяка функція ShowResult(), в яку передається покажчик на базовий клас. Функція викликає віртуальну функцію Calc() за покажчиком. У тілі функції ShowResult() не відомо, екземпляр якого класу буде їй передано. Екземпляр буде сформовано сформовано під час виконання.

#include <iostream>
using namespace std;

// Клас, що містить функцію обчислення довжини кола
class CalcLength
{
public:
  // Віртуальна функція
  virtual double Calc(double radius)
  {
    return 2 * 3.1415 * radius;
  }
};

// Клас, що містить функцію обчислення площі круга
class CalcArea : public CalcLength
{
public:
  // Віртуальна функція
  double Calc(double radius) override
  {
    return 3.1415 * radius * radius;
  }
};

// Клас, що містить функцію обчислення об'єму кулі
class CalcVolume : public CalcArea
{
public:
  // Віртуальна функція
  double Calc(double radius) override
  {
    return 4.0 / 3 * 3.1415 * radius * radius * radius;
  }
};

// Деяка функція, яка отримує покажчик на базовий клас ClassLength та параметр радіусу,
// у даній функції демонструється поліморфізм
void ShowResult(CalcLength* p, double radius)
{
  // Виклик методу Calc() за покажчиком p.
  // Для даної функції невідомо, метод якого класу буде викликано.
  // Потрібний метод буде сформований під час виконання - це є поліморфізм
  double res = p->Calc(radius); // <=== спільний інтерфейс для різних реалізацій
  cout << "Result = " << res << endl;
}

void main()
{
  // 1. Оголосити покажчик на базовий клас - це важливо
  CalcLength* p = nullptr;

  // 2. Створити екземпляри 3-х класів
  CalcLength obj1;
  CalcArea obj2;
  CalcVolume obj3;

  // 3. Ввести номер функції
  int num;
  cout << "Enter number of function (1-3): ";
  cin >> num;

  if ((num < 1) || (num > 3))
    return;

  // 4. Ввести радіус
  double radius;
  cout << "radius = ";
  cin >> radius;

  // 5. Встановити покажчик p в залежності від введеного num
  if (num == 1) p = &obj1;
  if (num == 2) p = &obj2;
  if (num == 3) p = &obj3;

  // 6. Викликати метод ShowResult()
  // Потрібний об'єкт підставляється в залежності від ситуації
  ShowResult(p, radius);

  // 7. Викликати метод ShowResult() безпосередньо підставляючи екземпляр класу
  if (num == 1) ShowResult(&obj1, radius);
  if (num == 2) ShowResult(&obj2, radius);
  if (num == 3) ShowResult(&obj3, radius);
}

 

4.3. Приклад поліморфізму для трьох класів. Виклик віртуальної функції у методі. Передача в метод посилання (&) на базовий клас

У прикладі демонструється поліморфізм на основі посилання на базовий клас, яке передається у функцію DemoPolymorphism().

Оголошується 3 класи з іменами A, B, C. Клас A є базовим для класу B. Клас B є базовим для класу C. класи містять тільки один метод, який виводить назву класу. З допомогою механізму поліморфізму у функцію DemoPolymorphism() передається посилання на один з екземплярів. Відповідно до переданого екземпляру викликається потрібний метод.

#include <iostream>
using namespace std;

// Клас A - базовий клас в ієрархії A <= B <= C
class A
{
public:
  virtual void Show()
  {
    cout << "A::Show()" << endl;
  }
};

// Клас B - успадкований від класу A
class B : public A
{
public:
  // віртуальний метод - перевизначає однойменний метод класу A
  void Show() override
  {
    cout << "B::Show()" << endl;
  }
};

// Клас C - успадкований від класу B
class C : public B
{
public:
  // віртуальний метод - перевизначає однойменний метод класу B
  void Show() override
  {
    cout << "C::Show()" << endl;
  }
};

// Функція, яка отримує посилання на базовий клас A,
// у функції демонструється поліморфізм.
void DemoPolymorphism(A& ref)
{
  // Виклик віртуального методу, метод визначається при виконанні
  // тут невідомо, метод якого класу A, B чи C потрібно викликати.
  ref.Show();
}

void main()
{
  // 1. Оголосити екземпляри класів A, B, C
  A objA;
  B objB;
  C objC;

  // 2. Викликати функцію DemoPolymorphism(),
  //    в залежності від того, який екземпляр передається,
  //    викликається відповідний метод класу.
  DemoPolymorphism(objA); // A::Show()
  DemoPolymorphism(objB); // B::Show()
  DemoPolymorphism(objC); // C::Show()
}

Після запуску на виконання програма видасть наступний результат

A::Show()
B::Show()
C::Show()

Якщо при оголошенні методу Show() у класах A, B, C забрати специфікатори virtual та override, то поліморфізм підтримуватись не буде. А це означає, що при виклику методу DemoPolymorphism() усі передані посилання на екземпляри objA, objB, objC будуть конвертуватись у посилання на базовий клас A&. Як наслідок, 3 рази буде викликаний метод Show() класу A і програма видасть наступний результат

A::Show()
A::Show()
A::Show()

 


Споріднені теми