Виртуальная функция и механизм позднего связывания
Виртуальный метод (виртуальная функция) - в объектно-ориентированном программировании метод (функция) класса, который может быть переопределён в классах-наследниках так, что конкретная реализация метода для вызова будет определяться во время исполнения.
Виртуальные методы - один из важнейших приёмов реализации полиморфизма. Они позволяют создавать общий код, который может работать как с объектами базового класса, так и с объектами любого его класса-наследника. При этом базовый класс определяет способ работы с объектами, и любые его наследники могут предоставлять конкретную реализацию этого способа.
Виртуальная функция объявляется в базовом или в производном классе и, затем, переопределяется в наследуемых классах. Совокупность классов (подклассов), в которых определяется и переопределяется виртуальная функция, называется полиморфическим кластером, ассоциированным с некоторой виртуальной функцией. В пределах полиморфического кластера сообщение связывается с конкретной виртуальной функцией-методом во время выполнения программы.
Обычную функцию-метод можно переопределить в наследуемых классах. Однако без атрибута virtual такая функция-метод будет связана с сообщением на этапе компиляции. Атрибут virtual гарантирует позднее связывание в пределах полиморфического кластера.
Часто возникает необходимость передачи сообщений объектам, принадлежащим разным классам в иерархии. В этом случае требуется реализация механизма позднего связывания. Чтобы добиться позднего связывания для объекта, его нужно объявить как указатель или ссылку на объект соответствующего класса. Для открытых производных классов указатели и ссылки на объекты этих классов совместимыми с указателями и ссылками на объекты базового класса (т.е. к объекту производного класса можно обращаться, как будто это объект базового класса). Выбранная функция-метод зависит от класса, на объект которого указывается, но не от типа указателя.
С++ поддерживает virtual функции-методы, которые объявлены в основном классе и переопределены в порожденном классе. Иерархия классов, определенная общим наследованием, создает связанный набор типов пользователя, на которые можно ссылаться с помощью указателя базового класса. При обращении к виртуальной функции через этот указатель в С++ выбирается соответствующее функциональное определение во время выполнения. Объект, на который указывается, должен содержать в себе информацию о типе, поскольку различия между ними может быть сделано динамически. Это особенность типична для ООП кода. Каждый объект “знает” как на него должны воздействовать. Эта форма полиморфизма называется чистым полиморфизмом.
В С++ функции-методы класса с различным числом и типом параметров есть действительно различные функции, даже если они имеют одно и то же имя. Виртуальные функции позволяют переопределять в управляемом классе функции, введенные в базовом классе, даже если число и тип аргументов то же самое. Для виртуальных функций нельзя переопределять тип функции. Если две функции с одинаковым именем будут иметь различные аргументы, С++ будет считать их различными и проигнорирует механизм виртуальных функций. Виртуальная функция - обязательно метод класса.
Пример:
class Base
{
public:
void Method ()
{
cout << "Базовый класс\n";
}
};
class Derived : public Base
{};
// внутри main
Base b;
Derived d;
b.Method();
d.Method();
//-------- Вывод:
Базовый класс
Базовый класс
На экран будет выведено две строки Базовый класс. На этапе компиляции память выделяется для двух копий Method - для базового класса и для производного. Оба адреса привязываются к именам методов: Base::Method, Derived::Method. Т.е. когда в коде мы вызываем Method, то вызывается метод, соответствующий типу объекта. Чтобы увидеть, что для каждого объекта вызывается свой метод, давайте переопределим метод Derived::Method:
public:
void Method ()
{
cout << "Производный класс\n";
}
// внутри main
Base b;
Derived d;
b.Method();
d.Method();
//-------- Вывод:
Базовый класс
Производный класс
Следующий пример. Определения классов оставим без изменений. Поработаем с указателями:
Base* b = new Derived;
Derived* d = new Derived;
b->Method();
d->Method();
//-------- Вывод:
Базовый класс
Производный класс
Позднее/динамическое связывание
class Base
{
public:
virtual void Method ()
{
cout << "Базовый класс\n";
}
};
Вносить изменения в производные классы не нужно. Хотя можно и там добавить ключевое слово virtual (это не обязательно). Теперь посмотрим на наш код:
Base* b = new Derived;
Derived* d = new Derived;
b->Method();
d->Method();
//-------- Вывод:
Производный класс
Производный класс
Теперь вызывается метод того класса, на который на самом деле указывает указатель.
Несколько замечаний по виртуальным функциям:
-
Виртуальные функции используются только в классах. Поэтому часто используется название - виртуальные методы.
-
Если метод виртуальный и переопределен в вашем наследнике, то будет вызываться метод наследника.
-
В массив нужно объединять только те объекты, которые обладают методами с одинаковыми названиями, но разной реализацией (переопределение!!!).
Рассмотрим простой код:
class Base
{
public:
virtual void vf ()
{
cout << "Базовый класс\n";
}
};
class Derived : public Base
{
public:
void vf () // это тоже виртуальная функция
{
cout << "Производный класс\n";
}
};
Функции Base::vf и Derived::vf являются виртуальными. Об этом говорит ключевое слово virtual в базовом классе. А производный класс наследует это свойство для своего метода.
В С++ функции-методы класса с различным числом и типом параметров есть действительно различные функции, даже если они имеют одно и то же имя. Виртуальные функции позволяют переопределять в управляемом классе функции, введенные в базовом классе, даже если число и тип аргументов то же самое. Для виртуальных функций нельзя переопределять тип функции. Если две функции с одинаковым именем будут иметь различные аргументы, С++ будет считать их различными и проигнорирует механизм виртуальных функций. Виртуальная функция обязательно метод класса.
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 Base
{
public:
virtual void f(int x=1){...}
...
};
class Derived:public Base
{
public:
virtual void f(int x=2){...}
...
}
int main()
{
Derived d;
d.f(); //Будет вызван Derived::f(1); // Т.е. с аргументом по умолчанию заданным в Base!!!
}