Interested Article - Виртуальный метод

Полиморфизм
Специальный полиморфизм
Параметрический полиморфизм
Полиморфизм подтипов

Виртуальный метод ( виртуальная функция ) — в объектно-ориентированном программировании метод (функция) класса , который может быть переопределён в классах-наследниках так, что конкретная реализация метода для вызова будет определяться во время исполнения. Таким образом, программисту необязательно знать точный тип объекта для работы с ним через виртуальные методы: достаточно лишь знать, что объект принадлежит классу или наследнику класса, в котором объявлен метод. Одним из переводов слова virtual с английского языка может быть «фактический», что больше подходит по смыслу. [ источник не указан 1255 дней ]

Использование

Виртуальные методы — один из важнейших приёмов реализации полиморфизма . Они позволяют создавать общий код, который может работать как с объектами базового класса, так и с объектами любого его класса-наследника. При этом базовый класс определяет способ работы с объектами, и любые его наследники могут предоставлять конкретную реализацию этого способа.

Способ определения

Одни языки программирования (например, C++ , C# , Delphi ) требуют явно указывать, что данный метод является виртуальным. В других языках (например, Java , Python ) все методы являются виртуальными по умолчанию (но только те методы, для которых это возможно; например, в Java методы с доступом private не могут быть переопределены в связи с правилами видимости).

Базовый класс может и не предоставлять реализации виртуального метода, а только декларировать его существование. Такие методы без реализации называются «чистыми виртуальными» (перевод англ. pure virtual ) или абстрактными. Класс, содержащий хотя бы один такой метод, тоже будет абстрактным . Объект такого класса создать нельзя (в некоторых языках допускается, но вызов абстрактного метода приведёт к ошибке). Наследники абстрактного класса должны предоставить реализацию для всех его абстрактных методов, иначе они, в свою очередь, будут абстрактными классами. Абстрактный класс, который содержит только абстрактные методы, называется интерфейсом .

Реализация

Техника вызова виртуальных методов называется ещё «динамическим (поздним) связыванием». Имеется в виду, что имя метода, использованное в программе, связывается с адресом входа конкретного метода динамически (во время исполнения программы), а не статически (во время компиляции), так как в момент компиляции, в общем случае, невозможно определить, какая из существующих реализаций метода будет вызвана.

В компилируемых языках программирования динамическое связывание выполняется обычно с использованием таблицы виртуальных методов , которая создаётся компилятором для каждого класса, имеющего хотя бы один виртуальный метод. В элементах таблицы находятся указатели на реализации виртуальных методов, соответствующие данному классу (если в классе-потомке добавляется новый виртуальный метод, его адрес добавляется в таблицу, если в классе-потомке создаётся новая реализация виртуального метода - соответствующее поле в таблице заполняется адресом этой реализации). Таким образом, для адреса каждого виртуального метода в дереве наследования имеется одно, фиксированное смещение в таблице виртуальных методов. Каждый объект имеет техническое поле, которое при создании объекта инициализируется указателем на таблицу виртуальных методов своего класса. Для вызова виртуального метода из объекта берётся указатель на соответствующую таблицу виртуальных методов, а из неё, по известному фиксированному смещению, — указатель на реализацию метода, используемого для данного класса. При использовании множественного наследования ситуация несколько усложняется за счёт того, что таблица виртуальных методов становится нелинейной.

Пример виртуальной функции на C++

Диаграмма класса Animal

Пример на C++ , иллюстрирующий отличие виртуальных функций от невиртуальных:

Предположим, базовый класс Animal (животное) может иметь виртуальный метод eat (есть, питаться, принимать пищу). Подкласс (класс-потомок) Fish (рыба) переопределит метод eat() не так, как его переопределит подкласс Wolf (волк), но можно вызвать eat() на любом экземпляре класса, унаследованного от класса Animal , и получить поведение eat() , соответствующее данному подклассу.

Это позволяет программисту обрабатывать список объектов класса Animal , вызывая над каждым объектом метод eat() , не задумываясь о том к какому подклассу принадлежит текущий объект (то есть как питается конкретное животное).

Интересной деталью виртуальных функций в C++ является поведение аргументов по умолчанию . При вызове виртуальной функции с аргументом по умолчанию тело функции берется у реального объекта, а значения аргументов по типу ссылки или указателя.

class Animal {
public:
    void /*невиртуальный*/ move() { 
        std::cout << "This animal moves in some way" << std::endl; 
    }
    virtual void eat() {
        std::cout << "Animal eat something!" << std::endl; 
    }
    virtual ~Animal(){} // деструктор
};

class Wolf : public Animal {
public:
    void move() { 
        std::cout << "Wolf walks" << std::endl; 
    }
    void eat(void) { // метод eat переопределён и тоже является виртуальным
        std::cout << "Wolf eats meat!" << std::endl; 
    }
};

int main() {
    Animal* zoo[] = {new Wolf(), new Animal()};
    for(Animal* a:zoo) {
        a->move();
        a->eat();
        delete a; // Так как деструктор виртуальный, для каждого 
                  // объекта вызовется деструктор его класса
    }
    return 0;
}

Вывод:

This animal moves in some way
Wolf eats meat!
This animal moves in some way
Animal eat something!

Пример аналога виртуальных функций в PHP

Аналогом в PHP можно считать использование позднего статического связывания.

class Foo {
    public static function baz() {
        return 'вода';
    }
    public function __construct() {
        echo static::baz(); // позднее статическое связывание
    }
}

class Bar extends Foo {
    public static function baz() {
        return 'огонь';
    }
}

new Foo(); // выведет 'вода'
new Bar(); // выведет 'огонь'

Пример виртуальной функции в Delphi

полиморфизм языка Object Pascal , использующемся в Delphi . Рассмотрим пример:

Объявим два класса. Предка (Ancestor):

 TAncestor = class
 private
 protected
 public
   {Виртуальная процедура.} 
   procedure VirtualProcedure; virtual; 
   procedure StaticProcedure;
 end;

и его потомка (Descendant):

 TDescendant = class(TAncestor)
 private
 protected
 public
    {Перекрытие виртуальной процедуры.}
   procedure VirtualProcedure; override;
   procedure StaticProcedure;
 end;

Как видно в классе предке объявлена виртуальная функция — VirtualProcedure . Чтобы воспользоваться достоинствами полиморфизма, её нужно перекрыть в потомке.

Реализация выглядит следующим образом:

 { TAncestor }
   
 procedure TAncestor.StaticProcedure;
 begin
   ShowMessage('Ancestor static procedure.');
 end;
   
 procedure TAncestor.VirtualProcedure;
 begin
   ShowMessage('Ancestor virtual procedure.');
 end;
 { TDescendant }
   
 procedure TDescendant.StaticProcedure;
 begin
   ShowMessage('Descendant static procedure.');
 end;
   
 procedure TDescendant.VirtualProcedure;
 begin
   ShowMessage('Descendant override procedure.');
 end;

Посмотрим как это работает:

 procedure TForm2.BitBtn1Click(Sender: TObject);
 var
   MyObject1: TAncestor;
   MyObject2: TAncestor;
 begin
   MyObject1 := TAncestor.Create;
   MyObject2 := TDescendant.Create;
   try
     MyObject1.StaticProcedure;
     MyObject1.VirtualProcedure;
     MyObject2.StaticProcedure;
     MyObject2.VirtualProcedure;
   finally
     MyObject1.Free;
     MyObject2.Free;
   end;
 end;

Заметьте, что в разделе var мы объявили два объекта MyObject1 и MyObject2 типа TAncestor . А при создании MyObject1 создали как TAncestor , а MyObject2 как TDescendant . Вот что мы увидим при нажатии на кнопку BitBtn1 :

  1. Ancestor static procedure.
  2. Ancestor virtual procedure.
  3. Ancestor static procedure.
  4. Descendant override procedure.

Для MyObject1 все понятно, просто вызвались указанные процедуры. А вот для MyObject2 это не так.

Вызов MyObject2.StaticProcedure; привел к появлению «Ancestor static procedure.». Ведь мы объявили MyObject2: TAncestor , поэтому и была вызвана процедура StaticProcedure; класса TAncestor .

А вот вызов MyObject2.VirtualProcedure; привел к вызову VirtualProcedure; реализованной в потомке( TDescendant ). Это произошло потому, что MyObject2 был создан не как TAncestor , а как TDescendant : MyObject2 := TDescendant.Create; . И виртуальный метод VirtualProcedure был перекрыт.

В Delphi полиморфизм реализован с помощью так называемой виртуальной таблицы методов (или VMT).

Достаточно часто виртуальные методы забывают с помощью ключевого слова override . Это приводит к метода. В этом случае замещения методов в VMT не произойдет и требуемая функциональность не будет получена.

Эта ошибка отслеживается компилятором, который выдаёт соответствующее предупреждение.

Пример виртуального метода на C#

Пример виртуального метода на C#. В примере используется ключевое слово base , предоставляющее доступ к методу a() родительского (базового) класса A .

class Program
{
    static void Main(string[] args)
    {
        A myObj = new B();
        Console.ReadKey();
    }        
}

//Базовый класс A
public class A
{
    public virtual string a()
    {
        return "огонь";
    }
}

//Произвольный класс B, наследующий класс A
class B : A
{
    public override string a()
    {
        return "вода";
    }

    public B()
    {
        //Выводим результат возвращаемый переопределённым методом
        Console.Out.WriteLine(a()); //вода
        //Выводим результат возвращаемый методом родительского класса
        Console.Out.WriteLine(base.a());    //огонь
    }
}

Вызов метода предка из перекрытого метода

Бывает необходимо вызвать метод предка в перекрытом методе.

Объявим два класса. Предка(Ancestor):

 TAncestor = class
 private
 protected
 public
   {Виртуальная процедура.} 
   procedure VirtualProcedure; virtual; 
 end;

и его потомка (Descendant):

 TDescendant = class(TAncestor)
 private
 protected
 public
    {Перекрытие виртуальной процедуры.}
   procedure VirtualProcedure; override;
 end;

Обращение к методу предка реализуется с помощью ключевого слова «inherited»

 procedure TDescendant.VirtualProcedure;
 begin
     inherited;
 end;

Стоит помнить, что в Delphi деструктор должен быть обязательно перекрытым — «override» — и содержать вызов деструктора предка

TDescendant = class(TAncestor)
 private
 protected
 public
    destructor Destroy; override;
 end;
 destructor TDescendant. Destroy;
 begin
     inherited;
 end;

В языке C++ не нужно вызывать конструктор и деструктор предка, деструктор должен быть виртуальным. Деструкторы предков вызовутся автоматически. Чтобы вызвать метод предка, нужно явно вызвать метод:

class Ancestor
{
public:
  virtual void  function1 () { printf("Ancestor::function1"); }
};

class Descendant : public Ancestor
{
public:
  virtual void  function1 () {
     printf("Descendant::function1");
     Ancestor::function1(); // здесь будет напечатано "Ancestor::function1"
  }
};

Для вызова конструктора предка нужно указать конструктор:

class Descendant : public Ancestor
{
public:
  Descendant(): Ancestor(){}
};


Ещё примеры

Первый пример
class Ancestor
{
public:
  virtual void  function1 () { cout << "Ancestor::function1()" << endl; }
  void          function2 () { cout << "Ancestor::function2()" << endl; }
};

class Descendant : public Ancestor
{
public:
  virtual void  function1 () { cout << "Descendant::function1()" << endl; }
  void          function2 () { cout << "Descendant::function2()" << endl; }
};

Descendant*  pointer      = new Descendant ();
Ancestor*    pointer_copy = pointer;

pointer->function1 ();
pointer->function2 ();

pointer_copy->function1 ();
pointer_copy->function2 ();

В этом примере класс Ancestor определяет две функции, одну из них виртуальную, другую — нет. Класс Descendant переопределяет обе функции. Однако, казалось бы одинаковое обращение к функциям даёт разные результаты. На выводе программа даст следующее:

    Descendant::function1()
    Descendant::function2()
    Descendant::function1()
    Ancestor::function2()

То есть, для определения реализации виртуальной функции используется информация о типе объекта и вызывается «правильная» реализация, независимо от типа указателя. При вызове невиртуальной функции, компилятор руководствуется типом указателя или ссылки, поэтому вызываются две разные реализации function2() , несмотря на то, что используется один и тот же объект.

Следует отметить, что в C++ можно, при необходимости, указать конкретную реализацию виртуальной функции, фактически вызывая её невиртуально:

pointer->Ancestor::function1 ();

для нашего примера выведет Ancestor::function1() , игнорируя тип объекта.

Второй пример
class A
{
public:
     virtual int function () {
        return 1;
     }
     
     int get() {
         return this->function();
     }
};
 
class B: public A
{
public:
     int function() {
        return 2;
     }
};

#include <iostream>

int main() {
   B b; 
 
   std::cout << b.get() << std::endl; // 2
   
   return 0;
}

Несмотря на то, что в классе B отсутствует метод get() , его можно позаимствовать у класса A , при этом результат работы этого метода вернет вычисления для B::function() !

Третий пример
#include <iostream>
using namespace std;

struct IBase
{
	virtual void foo(int n=1) const = 0;
	virtual ~IBase() = 0;
};

void IBase::foo(int n) const {
	cout << n << " foo\n";
} 

IBase::~IBase() {
	cout << "Base destructor\n";
}

struct Derived final : IBase
{
	virtual void foo(int n=2) const override final {
		IBase::foo(n);
	}
};

void bar(const IBase& arg)
{
	arg.foo();
}

int main () {
	bar(Derived());
	return 0;
}

В данном примере показан пример создания интерфейса IBase. На примере интерфейса показана возможность создания абстрактного класса, не имеющего виртуальных методов: при объявлении деструктора чистым виртуальным и вынесении его определения за тело класса пропадает возможность создавать объекты такого класса, но остаётся возможность создавать потомки данного предка.

Вывод программы будет следующим: 1 foo\nBase destructor\n . Как мы видим, значение аргумента по-умолчанию взялось от типа ссылки, а не от реального типа объекта. Также как и деструктор.

Ключевое слово final показывает, что класс или метод нельзя переопределять, а override, что виртуальный метод явно переопределён.

См. также

Примечания

  1. . Дата обращения: 16 сентября 2020. 24 сентября 2020 года.
  2. . php.net. Дата обращения: 5 ноября 2016. 8 ноября 2016 года.

Ссылки

  • (англ.)
  • / Конспекты курса по C++ 2010/11, Санкт-Петербургское отделение Математического института им. В.А.Стеклова РАН
Источник —

Same as Виртуальный метод