Термин «виртуальный» означает видимый, но не существующий в реальности. Когда используются виртуальные функции, программа, которая, казалось бы, вызывает функцию одного класса, может в этот момент вызвать функцию совсем другого класса. Допустим, имеется набор объектов разных классов, но вам хочется, чтобы они все были в одном массиве и вызывались с помощью одного и того же выражения. Например, есть набор классов геометрических фигур: треугольников, шаров, квадратов и т.п., и в каждом из этих классов есть функция Draw(), отвечающая за отрисовку объекта на экране. Используя механизм виртуальных функций, вы можете нарисовать картинку, содержащую различные геометрические фигуры, с помощью простого цикла:
Shape *pFigure[100];//Shape-некоторый базовый класс
…
for (int i=0; i
pFigure[i]->Draw(); /* pFigure-массив указателей на какие-то различные геометрические фигуры */
То есть абсолютно разные функции выполняются с помощью одного и того же вызова. Так если указатель в массиве pFigure указывает на шарик, вызывается функция, рисующая шарик, если он указывает на треугольник – рисуется треугольник. Вот это и есть полиморфизм, то есть различные формы. Полиморфизм – это одна из ключевых особенностей объектно-ориентированного программирования, наряду с классами и наследованием.
Чтобы использовать полиморфизм, необходимо выполнять некоторые условия. Во-первых, все классы должны быть наследниками одного и того же базового класса, в нашем примере это – Shape. Во-вторых, функция Draw() должна быть объявлена виртуальной (спецификатор virtual) в базовом классе.
Спецификатор virtual при объявлении функции-элемента предписывает компилятору генерировать некоторую дополнительную информацию о функции. Если функция переопределяется в производном классе и вызывается с указателем (или ссылкой) базового класса, ссылающимся на представитель производного класса, эта информация позволяет определить, какой из вариантов функции должен быть выбран: такой вызов будет адресован функции производного класса. Например:
#include <iostream>
using namespace std;
class Base //Базовый класс
{
public:
void show() //Обычная функция
{ cout << "Base\n"; }
};
class Derv1: public Base //Производный класс 1
{
public:
void show()
{ cout << "Derv1\n"; }
};
class Derv2: public Base //Производный класс 2
{
public:
void show()
{ cout << "Derv2\n"; }
};
int main()
{
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс
ptr = &dv1; //Адрес dv1 занести в указатель
ptr->show() //Выполнить show()
ptr = &dv2; //Адрес dv2 занести в указатель
ptr->show() //Выполнить show()
return 0;
}
Классы Derv1 и Derv2 являются наследниками класса Base. В каждом из этих трех классов имеется метод show(). В main мы создаем объекты классов Derv1 и Derv2, а также указатель на класс Base. Затем адрес порожденного объекта заносится в указатель базового класса. Эта операция является полностью корректной, так как указатели на объекты порожденных классов совместимы по типу с указателями на объекты базового класса.
Что же получится в результате выполнения такой программы? А в результате, несмотря на то, что указателю на базовый класс присваивали адреса производных классов, все равно будет выполняться метод базового класса, то есть на экран будет выведено:
Base
Base
То есть компилятор не смотрит на содержимое указателя ptr, а выбирает тот метод, который удовлетворяет типу указателя.
Теперь внесем одно маленькое дополнение в текст программы: поставим ключевое слово virtual перед объявлением функции show() в базовом классе:
#include <iostream>
using namespace std;
class Base //Базовый класс
{
public:
virtual void show() //Виртуальная функция
{ cout << "Base\n"; }
};
class Derv1: public Base //Производный класс 1
{
public:
void show()
{ cout << "Derv1\n"; }
};
class Derv2: public Base //Производный класс 2
{
public:
void show()
{ cout << "Derv2\n"; }
};
int main()
{
Derv1 dv1; //Объект производного класса 1
Derv2 dv2; //Объект производного класса 2
Base* ptr; //Указатель на базовый класс
ptr = &dv1; //Адрес dv1 занести в указатель
ptr->show(); //Выполнить show()
ptr = &dv2; //Адрес dv2 занести в указатель
ptr->show() //Выполнить show()
return 0;
}
В результате выполнения на экран будет выведено:
Derv1
Derv2
То есть выполняются методы производных классов, а не базового. Следовательно, после введения ключевого слова virtual в объявление функции, компилятор начал выбирать функцию, удовлетворяющую тому, что занесено в указатель, а не типу указателя, как это было в предыдущем примере.
В момент компиляции программы компилятор не генерирует вызов функции show(), т.к. не знает, к какому классу отнести содержимое указателя ptr, и откладывает принятие решения до фактического запуска программы. А уже в момент выполнения, когда известно, на что указывает ptr, вызывается соответствующая версия show(). Такой подход называют поздним связыванием или динамическим связыванием. Выбор функции в обычном порядке, во время компиляции, называется ранним или статическим связыванием. Позднее связывание требует больше ресурсов, но дает выигрыш в возможностях и гибкости. Для виртуальных функций можно указать следующие правила:
• виртуальную функцию нельзя объявить как staic;
• спецификатор virtual необязателен при переопределении функции в производном классе;
• виртуальная функция должна быть либо определена, либо описываться как чистая.
Таким образом, виртуальные функции предоставляют программисту возможность объявить в базовом классе функции, которые можно заместить в каждом производном классе. Компилятор и загрузчик гарантируют правильное соответствие между объектами и функциями, применяемыми к ним.
Для того чтобы объявление виртуальной функции работало в качестве интерфейса к функциям, определенным в производным классах, типы аргументов функций в производном классе не должны отличаться от типов аргументов, объявленных в базовом классе, и только очень небольшие изменения допускаются для типа возвращаемого значения. Например, если исходный тип возвращаемого значения был B*, то тип возвращаемого значения замещающей функцией может быть D* при условии, что B является открытым базовым классом для D. Аналогично вместо B& тип возвращаемого значения может быть ослаблен до D&. Обратите внимание, что подобные правила «ослабления типа» для аргументов привели бы к нарушениям типов.
Виртуальная функция должна быть определена для класса, в котором она впервые объявлена (исключение – чистые виртуальные функции). Виртуальную функцию можно использовать, даже если у ее класса нет производных классов. Производный класс, который не нуждается в собственной версии виртуальной функции, не обязан ее реализовывать. При создании производного класса вы можете реализовать требуемую функцию, только если в этом есть необходимость.
Для того чтобы обойти механизм виртуальных функций, при вызове используется имя класса с оператором разрешения области видимости. Например:
#include <stdio.h>
class Base
{
public:
virtual void Virt()
{ printf(Base::Vitr()); }
void nonVirt()
{ printf(Base::nonVitr()); }
};
class Derived: public Base
{
public:
void Virt()
{ printf(Derived::Vitr()); }
void nonVirt()
{ printf(Derived::nonVitr()); }
};
int main(void)
{
/* базовый указатель, реально ссылающийся на произ-водный класс */
Base *bp = new Derived;
// вызов виртуальной функции
bp->Virt(); // будет напечатано Derived::Virt()
bp->Base::Virt();// будет напечатано Base::Virt()
// вызов не-виртуальной функции
bp->nonVirt();// будет напечатано Base::nonVirt()
return 0;
}
За исключением случаев, когда мы явно указываем, с помощью оператора разрешения области видимости, какая версия виртуальной функции должна вызываться, активизируется наиболее подходящий для вызывающего объекта вариант замещенной функции. Виртуальные функции реализуются с использованием таблицы переходов следующим образом.
• Для каждого класса, содержащего виртуальные функции, компилятор строит таблицу адресов этих функций, обычно называемую таблицей виртуальных методов.
• Каждый представитель класса с виртуальными функциями содержит (скрытый) указатель на его таблицу виртуальных методов.
• Компилятор автоматически вставляет в начало конструктора класса фрагмент кода, который инициализирует указатель виртуальных методов класса.
• Для любой данной иерархии классов адрес некоторой виртуальной функции имеет всегда одно и то же смещение в таблицах виртуальных методов каждого класса.
• Если вызывается виртуальная функция, код, сгенерированный компилятором, прежде всего находит указатель виртуальной таблицы. Затем код обращается к таблице виртуальных методов и извлекает из нее адрес виртуальной функции, после чего производится косвенный вызов функции.
Опубликовал Kest
August 27 2010 12:35:44 ·
1 Комментариев ·
11330 Прочтений ·
• Не нашли ответ на свой вопрос? Тогда задайте вопрос в комментариях или на форуме! •
Комментарии
Спасибо April 16 2011 17:13:15
Спасибо!
Добавить комментарий
Рейтинги
Рейтинг доступен только для пользователей.
Пожалуйста, залогиньтесь или зарегистрируйтесь для голосования.
Нет данных для оценки.
Гость
Вы не зарегистрированны? Нажмите здесь для регистрации.