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 предназначена для переопределения одноименной виртуальной функции базового класса. Согласно этому, компилятор будет контролировать всю цепочку объявлений вида virtualoverride.

Если для функции, объявленной со спецификатором 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()

 


Связанные темы