C++. Абстрактний клас. Чисто віртуальна функція

Абстрактний клас. Чисто віртуальна функція.  Ключове слово abstract. Приклади

Перед вивченням даної теми рекомендується ознайомитись з наступною темою:


Зміст


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

1. Чисто віртуальна функція. Поняття

Перед вивченням поняття абстрактного класу потрібно ознайомитись з поняттям “чисто віртуальна функція”.

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

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

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

У синтаксисі мови C++ є можливість оголосити чисту віртуальну функцію. Чиста віртуальна функція – це функція, яка:

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

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

class ClassName
{
  virtual return_type FuncNamePure(list_of_parameters) = 0;
};

де

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

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

...

// Це не є чисто віртуальна функція
virtual return_type FuncName(list_of_parameters)
{ }

...

 

2. Абстрактний клас. Загальні поняття. Особливості оголошення та використання

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

Бувають випадки, коли класи верхніх рівнів ієрархії містять одні тільки чисто віртуальні функції і більше ніяких. Таким чином, клас, що містить загальні або чисто віртуальні функції нічого не робить, він є лише зв’язною ланкою для забезпечення поліморфізму. Отже, не має сенсу створювати екземпляр такого класу, оскільки об’єкт цього класу є непотрібним. Цей об’єкт займає місце в пам’яті, але не виконує безпосередньо ніякої роботи. У мові C++ є можливість обмежити використання об’єктів “пустих” класів шляхом оголошення їх абстрактними.

Таким чином, можна дати визначення абстрактного класу. Абстрактний клас – це клас, який містить хоча б одну чисто віртуальну функцію. Компілятор цей клас визначає по особливому. Створити об’єкт абстрактного класу не вдасться. Спроба створення такого об’єкту буде розпізнана компілятором як помилка. Це є логічно, навіщо виділяти пам’ять під елемент, який не визначає коду, що надає послуги.

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

class ClassName
{
  virtual return_type Func(list_of_parameters) = 0;

  // Інші елементи класу
  // ...
}

тут

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

Спроба оголосити екземпляр класу ClassName призведе до помилки на етапі компіляції

ClassName obj; // помилка компіляції, заборонено

Однак, допускається оголошувати покажчик або посилання на абстрактний клас

ClassName* p; // оголошення покажчика на абстрактний клас ClassName
ClassName& ref = obj; // оголошення посилання на абстрактний клас ClassName

У випадку оголошення посилання на абстрактний клас ClassName, це посилання повинно бути ініціалізоване одразу значенням адреси екземпляру будь-якого неабстрактного класу, який є успадкованим від класу ClassName.

 

3. Ключове слово abstract. Застосування до класу. Застосування до функції. Приклад

У C++ існує інша форма вказання факту, що клас є абстрактний – це задання ключового слова abstract після імені класу. Для класу з іменем  AbstractClass використання ключового слова abstract може бути таким

class AbstractClass abstract
{
  // ...
}

У вищенаведеному контексті ключове слово abstract вказує на наступне:

  • клас є абстрактним – створити екземпляр абстрактного класу не вийде (інформативна складова);
  • клас (тип) може бути використаний в якості базового типу;
  • функції у класі можуть бути віртуальними. Не обов’язково клас, що оголошений як abstract, повинен містити віртуальні функції.

Також в абстрактному класі (з ключовим словом abstract або без нього) чисті віртуальні функції можуть бути оголошені також з словом abstract. Наприклад, для класу AbstractClass може бути таке оголошення

class AbstractClass abstract
{
  // ...
  void SomeVirtualFunc() abstract;
}

У вищенаведеному коді оголошення функції SomeVirtualFunc() з ключовим словом abstract аналогічно оголошенню

...
void SomeVirtualFunc() = 0;
...

Приклад.

#include <iostream>
using namespace std;

// Ключове слово abstract
class A abstract
{
public:
  void Print()
  {
    cout << "A::Print" << endl;
  }

  // Чисто віртуальна функція
  virtual void Print2() = 0;

  // Функція, реалізація якої повинна бути в успадкованому класі 
  virtual void Print3() abstract; // - таке саме як чисто віртуальна функція
};

// Похідний клас
class B : public A
{
public:
  // Перевизначення функції Print2()
  void Print2() override
  {
    cout << "B::Print2" << endl;
  }

  // Перевизначення функції Print3()
  void Print3() override
  {
    cout << "B::Print3" << endl;
  }
};

void main()
{
  //A obj;
  B obj;
  obj.Print();
  obj.Print2();
  obj.Print3();
}

Результат

A::Print
B::Print2
B::Print3

 

4. Приклад, що демонструє особливості використання абстрактного класу в програмі

У прикладі оголошується три класи, що утворюють ієрархію (рисунок 1):

  • базовий абстрактний клас A. Цей клас містить чисто віртуальну функцію Func(). Екземпляр цього класу створити неможливо;
  • не абстрактний клас B, що є успадкований від класу A. У класі B оголошується віртуальна функція Func(), яка перевизначає однойменну функцію базового класу A;
  • не абстрактний клас C, що є успадкований від класу B. У класі C визначено віртуальну функцію з іменем Func(). Ця функція перевизначає функцію Func() класу B.

 

C++. Абстрактний клас. Ієрархія класів та оголошення віртуальних функцій

Рисунок 1. Абстрактний клас. Ієрархія класів та оголошення віртуальних функцій

Програмний код класів, що відповідають рисунку 1.

#include <iostream>
using namespace std;

// Це є абстрактний клас, екземпляр (об'єкт) цього класу
// створити не вдасться
class A
{
public:
  // чисто віртуальна функція
  virtual void Func() = 0;
};

// Клас, що є успадкований від класу A
class B : public A
{
public:
  // перевизначити чисто віртуальну функцію - це обов'язково (вимога компілятора)
  void Func() override
  {
    cout << "B::Func()" << endl;
  }
};

// Клас, що успадкований від класу B
class C : public B
{
public:
  // перевизначити чисто віртуальну функцію
  void Func() override
  {
    cout << "C::Func()" << endl;
  }
};

int main()
{
  // 1. Спроба створення екземпляру абстрактного класу A
  // A objA; - заборонено, помилка компіляції

  // 2. Спроба створення екземплярів похідних класів B, C
  B objB; // можна, оскільки клас B не абстрактний
  C objC; // можна, оскільки клас C не абстрактний

  // 3. Оголосити покажчик на абстрактний клас A
  A* pA; // дозволено

  // 3.1. Перемістити покажчик pA на екземпляр похідного класу C
  pA = &objC;
  pA->Func(); // C::Func() - пізнє зв'язування, поліморфізм

  // 3.2. Перемістити покажчик pA на екземпляр похідного класу B
  pA = &objB;
  pA->Func(); // B::Func() - поліморфізм у дії

  // 4. Оголосити посилання refA на абстрактний клас A та ініціалізувати його
  //    значенням екземпляру похідного класу B
  A& refA = objB;
  refA.Func();   // B::Func() - поліморфізм у дії
}

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

C::Func()
B::Func()
B::Func()

 

5. Приклад демонстрації використання абстрактного класу при реалізації спадковості: Figures=>Circle=>CircleColor

У прикладі продемонстровано фрагмент ієрархії побудови геометричних фігур. У вершині ієрархії лежить абстрактний клас Figures. Цей клас використовується для забезпечення коректної роботи механізму віртуальних функцій. Клас Figures визначає геометричну фігуру в загальному. Більш конкретна фігура (коло, прямокутник, трикутник тощо) може бути визначена в успадкованих класах. Відповідно до цього, клас Figures містить оголошення віртуальної функції Area(), яка призначена для отримання площі фігури і має бути перевизначена в успадкованих класах. Оскільки, на цьому рівні ієрархії (у цьому класі) ще не відомо площу саме якої фігури потрібно обчислювати (площу круга, площу прямокутника тощо) то функція Area() носить загальний характер і визначена як чисто віртуальна функція. На основі визначення функції Area() як чисто віртуальної, клас Figure буде розпізнаватись компілятором як абстрактний клас.

На рисунку 2 відображено ієархію класів даного прикладу.

C++. Абстрактні класи. Перевизначення методу Area() в успадкованому класі

Рисунок 2. Абстрактні класи. Перевизначення методу Area() в успадкованому класі

У майбутньому, з класу Figures можна успадковувати класи інших геометричних фігур (Triangle – трикутник, Rectangle – прямокутник тощо). У цих класах метод Area() буде також віртуальним але він матиме конкретний код обчислення площі на відміну від методу Area() класу Figures.

Програмний код, що демонструє взаємодію між абстрактним класом Figures та неабстрактними класами Circle та CircleColor наведено нижче.

#include <iostream>
using namespace std;

// C++. Абстрактний клас Figure
class Figure
{
  // 1. Внутрішні поля класу
private:
  string name; // назва фігури

public:
  // 2. Конструктор
  Figure(string _name) :name(_name)
  {
  }

  // 3. Методи доступу (геттери, сеттери)
  string GetName()
  {
    return name;
  }

  void SetName(string name)
  {
    this->name = name;
  }

  // 4. Чисто віртуальна функція Area() - площа фігури
  virtual double Area() = 0;
};

// Клас, який реалізує коло.
// Є похідний від класу Figure - розширює клас Figure
class Circle : public Figure
{
  // 1. Внутрішні приховані поля класу
private:
  double x, y; // Координати центру кола
  double r; // радіус кола

public:
  // 2. Конструктор класу
  Circle(double x, double y, double r) :Figure("Circle")
  {
    this->x = x;
    this->y = y;
    if (r > 0) this->r = r;
    else
      this->r = 1.0;
  }

  // 3. Методи доступу - геттери, сеттери
  void GetXYR(double& x, double& y, double& r)
  {
    x = this->x;
    y = this->y;
    r = this->r;
  }

  void SetXYR(double x, double y, double r)
  {
    this->x = x;
    this->y = y;

    // відкорегувати радіус якщо потрібно
    if (r > 0)
      this->r = r;
    else
      this->r = 1.0;
  }

  // 4. Віртуальна функція Area() - площа фігури
  double Area() override
  {
    const double Pi = 3.141592;
    return Pi * r * r;
  }
};

// Клас, який розширює можливості класу Circle - додає колір кола
class CircleColor : public Circle
{
  // 1. Внутрішні змінні класу
private:
  unsigned int color = 0; // колір кола

public:
  // 2. Конструктор
  CircleColor(double x, double y, double r, unsigned int color) :Circle(x, y, r)
  {
    this->color = color;
  }

  // 3. Методи доступу
  unsigned int GetColor()
  {
    return color;
  }

  void SetColor(unsigned int color)
  {
    this->color = color;
  }
};

void main()
{
  // 1. Клас Circle
  // 1.1. Створити екземпляр класу Circle
  Circle cr(1, 3, 2);

  // 1.2. Викликати метод Area() класу Circle
  double area = cr.Area();
  cout << "The instance of class Color:" << endl;
  cout << "area = " << area << endl;

  // 1.3. Взяти значення полів екземпляру cr
  double x, y, r;
  cr.GetXYR(x, y, r);

  // 1.4. Вивести значення полів на екран
  cout << "x = " << x << ", y = " << y << ", radius = " << r << endl;

  // 2. Екземпляр класу CircleColor
  // 2.1. Створити екземпляр класу CircleColor
  CircleColor crCol(2, 4, 6, 1);

  // 2.2. Отримати значення полів екземпляру crCol
  // 2.2.1. // взяти значення кольору     з методу класу CircleColor
  unsigned int col = crCol.GetColor();

  // 2.2.2. Взяти значення x, y, r з методу базового класу Color
  crCol.GetXYR(x, y, r);

  // 2.3. Вивести отримані значення на екран
  cout << "The instance of class ColorCircle: " << endl;
  cout << "color = " << col << endl;
  cout << "x = " << x << endl;
  cout << "y = " << y << endl;
  cout << "r = " << r << endl;

  // 2.4. Викликати метод Area() базового класу Circle
  cout << "area = " << crCol.Area() << endl;
}

Результат роботи програми

The instance of class Color:
area = 12.5664
x = 1, y = 3, radius = 2
The instance of class ColorCircle:
color = 1
x = 2
y = 4
r = 6
area = 113.097

 

6. Успадкування абстрактних класів

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

// базовий абстрактний клас
class BaseClass
{
  // Чисто віртуальна функція
  virtual return_type Func(list_of_parameters) = 0;
}

// похідний абстрактний клас
class DerivedClass : BaseClass
{
  // Перевизначення чисто віртуальної функції
  virtual return_type Func(list_of_parameters) override = 0;
}

тут

  • BaseClass – базовий абстрактний клас, що визначає чисто віртуальну функцію Func();
  • DerivedClass – похідний абстрактний клас, який перевизначає чисто віртуальну функцію Func();
  • Func() – чисто віртуальна функція;
  • return_type – тип, що повертає чисто віртуальна функція Func();
  • list_of_parameters – список параметрів чисто віртуальної функції Func().

Крім того, при успадкуванні абстрактного класу, будь-яка чисто віртуальна функція цього класу має бути перевизначена в похідному класі. У противному випадку компілятор видасть помилку.

Наприклад.

Якщо задано абстрактний клас з чисто віртуальною функцією

class BaseClass
{
  virtual void Func() = 0;
}

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

Випадок 1. Віртуальна функція містить деякий код (тіло функції). Тоді приблизний код успадкованого класу буде таким

class DerivedClass : BaseClass
{
  ...

  void Func() override
  {
    // обов'язково потрібно визначати override-функцію,
    // яка містить блок коду
    // ...
  }
}

Випадок 2. Віртуальна функція реалізована як чисто віртуальна функція (без тіла функції). У цьому випадку, ця чисто віртуальна функція має бути перевизначена в похідному класі в ієрархії.

class DerivedClass : BaseClass
{
  ...

  // продовження ланцюжка чисто віртуальних функцій
  virtual void Func() override = 0;
}

У вищенаведеному коді ключове слово virtual можна опустити.

 


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