Виртуальные функции c очень простое объяснение. Виртуальные методы, свойства и индексаторы

Сергей Малышев (aka Михалыч)

Часть 1. Общая теория виртуальных функций

Посмотрев на название этой статьи, вы можете подумать: "Хм! Кто же не знает, что такое виртуальные функции! Это же..." Если это так, можете смело бросить чтение прямо на этом месте.

А для тех, кто только начинает разбираться в тонкостях С++, но уже имеет, скажем, начальные познания о такой вещи, как наследование, и что-то слышал о полиморфизме, имеет прямой смысл почитать этот материал. Если вы поймете виртуальные функции, то получите ключ к разгадке секретов успешного объектно-ориентированного проектирования.

Вообще говоря - материал не очень сложный. И все о чем тут будет говориться, несомненно, можно найти в книгах. Проблема только в том, что вы, пожалуй, не найдете полного изложения всей проблемы в одной, или двух книгах. Для того чтобы написать о виртуальных функциях, мне пришлось "проштудировать" 6 разных изданий. И даже в этом случае я совсем не претендую на полноту изложения. В списке литературы я указываю только основные, те, что натолкнули меня на стиль изложения и содержание.

Весь материал я решил разделить на 3 части.
Давайте в первой части попытаемся разобраться в общей теории виртуальных функций. Посмотрим во второй части их применение (и их мощь и силу!) на каком-либо более-менее жизненном примере. Ну, и в третей части еще поговорим о такой вещи, как виртуальные деструкторы.

Так что же это такое?

Давайте для начала вспомним, как в классическом программировании на С вы можете передать объект данных в функцию. Ничего сложного в этом нет, надо только задать тип передаваемого объекта в то время, когда вы пишете код функции. То есть, чтобы описать поведение объектов, необходимо заранее знать и описать их тип. Сила ООП в этом случае проявляется в том, что вы можете писать виртуальные функции так, чтобы объект сам определял, какую функцию ему необходимо вызвать, во время выполнения программы.

Говоря иными словами - с помощью виртуальных функций объект сам определяет свое поведение (собственные действия). Технику использования виртуальных функций как раз и называют полиморфизмом. Буквально полиморфизм означает обладание многими формами. Объект в вашей программе в действительности может представлять не один класс, а множество различных классов, если они связаны механизмом наследования с общим базовым классом. Ну и поведение объектов этих классов в иерархии, конечно же будет разным.

Ну вот, а теперь к делу!

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

В программе объекты этих классов могут быть объявлены, например, таким образом.

A object_A; //объявление объекта типа А
B object_B; //объявление объекта типа В
C object_C; //объявление объекта типа С

Согласно данному правилу указатель типа А может ссылаться на любой из этих трех объектов. То есть вот это будет верным:


point_to_Object=&object_C; //присвоим указателю адрес объекта С

А вот это уже не правильно:

В *point_to_Object; // объявим указатель на производный класс
point_to_Object=&object_А; //нельзя присвоить указателю адрес базового объекта

Несмотря на то, что указатель point_to_Object имеет тип А*, а не С* (или В*), он может ссылаться на объекты типа С (или В). Может быть правило будет более понятным, если вы будете думать об объекте С, как особом виде объекта А. Ну, например, пингвин - это особая разновидность птиц, но он все таки остается птицей, хоть и не летает. Конечно, эта взаимосвязь объектов и указателей работает только в одном направлении. Объект типа С - особый вид объекта А, но вот объект А не является особым видом объекта С. Возвращаясь к пингвинам смело можно сказать, что если бы все птицы были особым видом пингвинов - они бы просто не умели летать!

Этот принцип становится особенно важным, когда в классах, связанных наследованием определяются виртуальные функции. Виртуальные функции имеют точно такой же вид и программируются так же, как и самые обычные функции. Только их объявление производится с ключевым словом virtual . Например, наш базовый класс А может объявить виртуальную функцию v_function() .

class A
{
public:
virtual void v_function(void);//функция описывает некое поведение класса А
};

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

Если в классе В, порожденном от класса А нужно описать коке-то другое поведение, то можно объявить виртуальную функцию, названную опять-таки v_function() .

class B: public A
{
public:
virtual void v_function(void);//замещающая функция описывает некое
//новое поведение класса В
};

Когда в классе, подобном В, определяется виртуальная функция, имеющая одинаковое имя с виртуальной функцией класса-предка, такая функция называется замещающей. Виртуальная функция v_function()в В замещает виртуальную функцию с тем же именем в классе А. На самом деле все несколько сложнее и не сводится к простому совпадению имен. Но об этом чуть позже, в разделе "Некоторые тонкости применения".
Ну, а теперь самое важное!

Вернемся к указателю point_to_Object типа А*, который ссылается на объект object_В типа В*. Давайте внимательно посмотрим на оператор, который вызывает виртуальную функцию v_function()для объекта, на который указывает point_to_Object .

A *point_to_Object; // объявим указатель на базовый класс
point_to_Object=&object_B; //присвоим указателю адрес объекта В
point_to_Object->;v_function(); //вызовем функцию

Указатель point_to_Object может хранить адрес объекта типа А или В. Значит во время выполнения этот оператор point_to_Object-gt;v_function(); вызывает виртуальную функцию класса на объект которого он в данный момент ссылается. Если point_to_Object ссылается на объект типа А, вызывается функция, принадлежащая классу А. Если point_to_Object ссылается на объект типа В, вызывается функция, принадлежащая классу В. Итак, один и тот же оператор вызывает функцию класса адресуемого объекта. Это и есть действие, определяемое во время выполнения программы.

Ну и что нам это дает?

Самое время посмотреть - а что же нам дают виртуальные функции? На теорию виртуальных функций в общих чертах мы взглянули. Пора рассмотреть какую-нибудь реальную ситуацию, где можно уяснить практическое значение рассматриваемого предмета в реальном мире программирования.

Классический пример (по моему опыту - в 90% всей литературы по С++), который приводят в этих целях - написание графической программы. Строится иерархия классов, что-то типа "точка -gt; линия -gt; плоская фигура -gt; объемная фигура". И рассматривается виртуальная функция, скажем, Draw(), которая рисует все это... Скучно!

Давайте рассмотрим менее академичный, но все же графический пример. (Классика! Куда от нее деться?). Попробуем рассмотреть гипотетически принцип, который может быть заложен в компьютерную игру. И не просто в игру, а в основу любого (не важно 3D или 2D, крутого или так себе) шутера. Стрелялки, проще говоря. Я не кровожаден по жизни, но, грешен, люблю иногда постреляться!

Итак, мы задумали сделать крутой шутер. Что понадобиться в первую очередь? Конечно же оружие! (Ну, пусть не в первую. Не важно.) В зависимости от того, на какую тему будем сочинять, такое оружие и понадобится. Может это будет набор от простой дубины до арбалета. Может от аркебуза до гранатомета. А может и вовсе от бластера до дезинтегратора. Скоро мы увидим, что это-то как раз и не важно.

Что же, раз есть такая масса возможностей, надо завести базовый класс.

class Weapon
{
public:
... //тут будут данные-члены, которыми может описываться, например, как
//толщина дубины, так и количество гранат в гранатомете
//эта часть для нас не важна

virtual void Use1(void);//обычно - левая кнопка мыши
virtual void Use2(void);//обычно - правая кнопка мыши

... //тут будут еще какие-то данные-члены и методы
};

Не вдаваясь в подробности этого класса, можно сказать, что самыми важными, пожалуй, будут функции Use1() и Use2(), которые описывают поведение (или применение) этого оружия. От этого класса можно порождать любые виды вооружения. Будут добавляться новые данные-члены (типа количества патронов, скорострельности, уровня энергии, длинны лезвия и т.п.) и новые функции. А переопределяя функции Use1() и Use2(), мы будем описывать различие в применении оружия (для ножа это может быть удар и метание, для автомата - стрельба одиночными и очередями).

Коллекцию вооружения надо где-то хранить. Видимо, проще всего организовать для этого массив указателей типа Weapon*. Для простоты предположим, что это глобальный массив Arms, на 10 видов оружия, и все указатели для начала инициализированы нулем.

Weapon *Arms; //массив указателей на объекты типа Weapon

Создавая в начале программы динамические объекты-виды оружия, будем добавлять указатели на них в массив.

Для того чтобы указать, какое оружие находится в пользовании, заведем переменную-индекс массива, значение которой будем изменять в зависимости от выбранного вида оружия.

int TypeOfWeapon;

В результате этих усилий, код, описывающий применение оружия в игре может выглядеть, например, так:

if(LeftMouseClick) Arms-gt;Use1();
else Arms->Use2();

Все! Мы создали код, который описывает стрельбу-пальбу-войну еще до того, как решили, какие типы оружия будут использоваться. Более того. У нас вообще еще нет ни одного реального типа вооружения! Дополнительная (иногда очень важная) выгода - этот код можно будет скомпилировать отдельно и хранить в библиотеке. В дальнейшем вы (или другой программист) можете вывести новые классы из Weapon, сохранить их в массиве Arms и использовать. При этом не потребуется перекомпиляции вашего кода.

Особо заметьте, что этот код не требует от вас точного задания типов данных объектов на которые ссылаются указатели Arms, требуется только, чтобы они были производными от Weapon. Объекты определяют во время выполнения, какую функцию Use() им следует вызвать.

Некоторые тонкости применения

Давайте немного времени уделим проблеме замещения виртуальных функций.

Вернемся к началу - к скучным классам А, В и С. Класс С на данный момент стоит у нас в самом низу иерархии, в конце линии наследования. В классе С точно также можно определить замещающую виртуальную функцию. Причем применять ключевое слово virtual совсем необязательно, поскольку это конечный класс в линии наследования. Функция и так будет работать и выбираться как виртуальная. Но! А вот если вам приспичит вывести некий класс D из класса С, да еще и изменить поведение функции v_function(), то тут как раз ничего и не выйдет. Для этого в классе С функция v_function() должна быть объявлена, как virtual. Отсюда правило, которое можно сформулировать так: "единожды виртуальный - всегда виртуальный!". То есть, ключевое слово virtual лучше не отбрасывать - вдруг пригодится?

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

Далее. Если в производном классе ввести функцию с тем же именем и типом возвращаемого значения, что и виртуальная функция базового класса, но с другим набором параметров, то эта функция производного класса уже не будет виртуальной. Даже если вы сопроводите ее ключевым словом virtual, она не будет тем, что вы ожидали. В этом случае с помощью указателя на базовый класс при любом значении этого указателя будет выполняться обращение к функции базового класса. Вспомните правило о перегрузке функций! Это просто разные функции. У вас получится совсем другая виртуальная функция. Вообще говоря подобные ошибки весьма трудноуловимы, поскольку обе формы записи вполне допустимы и надеяться на диагностику компилятора в этом случае не приходится.

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

И еще. Виртуальной функцией может быть только нестатическая компонентная функция класса. Виртуальной не может быть глобальная функция. Виртуальная функция может быть объявлена дружественной (friend) в другом классе. Но о дружественных функциях мы поговорим как-нибудь в другой статье.

Вот, собственно, и все на этот раз.

В следующей части вы увидите полностью функциональный пример простейшей программы, которая демонстрирует все те моменты, о которых мы говорили.

Если у вас есть вопросы - пишите, будем разбираться.

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

Для объявления виртуальной функции используется ключевое слово virtual . Функция-член класса может быть объявлена как виртуальная, если

  • класс, содержащий виртуальную функцию, базовый в иерархии порождения;
  • реализация функции зависит от класса и будет различной в каждом порожденном классе.

Это функция, которая определяется в базовом классе, а любой порожденный класс может ее переопределить. Виртуальная функция вызывается только через указатель или ссылку на базовый класс.

Определение того, какой экземпляр виртуальной функции вызывается по выражению вызова функции, зависит от класса объекта, адресуемого указателем или ссылкой, и осуществляется во время выполнения программы. Этот механизм называется динамическим (поздним) связыванием или разрешением типов во время выполнения .

Указатель на базовый класс может указывать либо на объект базового класса, либо на объект порожденного класса. Выбор функции-члена зависит от того, на объект какого класса при выполнении программы указывает указатель, но не от типа указателя. При отсутствии члена порожденного класса по умолчанию используется виртуальная функция базового класса.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#include
using namespace std;
class X
{
protected :
int i;
public :
void seti(int c) { i = c; }
virtual void print() { cout << endl << "class X: " << i; }
};
class Y: public X // наследование
{
public :
void print() { cout << endl << "class Y: " << i; } // переопределение базовой функции
};
int main()
{
X x;
X *px = &x; // Указатель на базовый класс
Y y;
x.seti(10);
y.seti(15);
px->print(); // класс X: 10
px = &y;
px->print(); // класс Y: 15
cin.get();
return 0;
}

Результат выполнения

В каждом случае выполняется различная версия функции print() . Выбор динамически зависит от объекта, на который ссылается указатель.

Если в строке 9 (см. код выше) убрать ключевое слово virtual , то результат выполнения будет уже другим, т.к. связывание функций будет происходить на этапе компиляции:

В терминологии ООП «объект посылает сообщение print и выбирает свою собственную версию соответствующего метода». Виртуальной может быть только нестатическая функция-член класса. Для порожденного класса функция автоматически становится виртуальной, поэтому ключевое слово virtual можно опустить.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

#include
using namespace std;
class figure
{
protected :
double x, y;
public :
figure(double a = 0, double b = 0) { x = a; y = b; }
virtual double area() { return (0); } // по умолчанию
};
class rectangle: public figure
{
public :
rectangle(double a = 0, double b = 0) : figure(a, b) {};
double area() { return (x*y); }
};
class circle: public figure
{
public :
circle(double a = 0) : figure(a, 0) {};
double area() { return (3.1415*x*x); }
};
int main()
{
figure *f;
rectangle rect(3, 4);
circle cir(2);
double total = 0;
f = ▭
f = ○
total = f->area();
cout << total << endl;
total += f->area();
cout << total << endl;
cin.get();
return 0;
}

Результат выполнения


Чистая виртуальная функция

Базовый класс иерархии типа обычно содержит ряд виртуальных функций, которые обеспечивают динамическую типизацию. Часто в самом базовом классе сами виртуальные функции фиктивны и имеют пустое тело. Определенное значение им придается лишь в порожденных классах. Такие функции называются чистыми виртуальными функциями .

Чистая виртуальная функция - это метод класса, тело которого не определено.

В базовом классе такая функция записывается следующим образом.

Виртуальные методы, свойства и индексаторы

Полиморфизм предоставляет подклассу способ определения собственной версии метода, определенного в его базовом классе, с использованием процесса, который называется переопределением метода (method overriding) . Чтобы пересмотреть текущий дизайн, нужно понять значение ключевых слов virtual и override.

Виртуальным называется такой метод, который объявляется как virtual в базовом классе. Виртуальный метод отличается тем, что он может быть переопределен в одном или нескольких производных классах. Следовательно, у каждого производного класса может быть свой вариант виртуального метода. Кроме того, виртуальные методы интересны тем, что именно происходит при их вызове по ссылке на базовый класс. В этом случае средствами языка C# определяется именно тот вариант виртуального метода, который следует вызывать, исходя из типа объекта, к которому происходит обращение по ссылке, причем это делается во время выполнения. Поэтому при ссылке на разные типы объектов выполняются разные варианты виртуального метода. Иными словами, вариант выполняемого виртуального метода выбирается по типу объекта, а не по типу ссылки на этот объект.

Так, если базовый класс содержит виртуальный метод и от него получены производные классы, то при обращении к разным типам объектов по ссылке на базовый класс выполняются разные варианты этого виртуального метода.

Метод объявляется как виртуальный в базовом классе с помощью ключевого слова virtual, указываемого перед его именем. Когда же виртуальный метод переопределяется в производном классе, то для этого используется модификатор override . А сам процесс повторного определения виртуального метода в производном классе называется переопределением метода. При переопределении метода - имя, возвращаемый тип и сигнатура переопределяющего метода должны быть точно такими же, как и у того виртуального метода, который переопределяется. Кроме того, виртуальный метод не может быть объявлен как static или abstract.

Переопределение метода служит основанием для воплощения одного из самых эффективных в C# принципов: динамической диспетчеризации методов , которая представляет собой механизм разрешения вызова во время выполнения, а не компиляции. Значение динамической диспетчеризации методов состоит в том, что именно благодаря ей в C# реализуется динамический полиморфизм.

Если при наличии многоуровневой иерархии виртуальный метод не переопределяется в производном классе, то выполняется ближайший его вариант, обнаруживаемый вверх по иерархии.

И еще одно замечание: свойства также подлежат модификации ключевым словом virtual и переопределению ключевым словом override. Это же относится и к индексаторам.

Давайте рассмотрим пример использования виртуальных методов, свойств и индексаторов:

// Реализуем класс содержащий информацию о шрифтах // и использующий виртуальные методы, свойства и индексаторы using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace ConsoleApplication1 { // Базовый класс class Font { string TypeFont; short FontSize; public Font() { TypeFont = "Arial"; FontSize = 12; } public Font(string TypeFont, short FontSize) { this.TypeFont = TypeFont; this.FontSize = FontSize; } public string typeFont { get { return TypeFont; } set { TypeFont = value; } } public short fontSize { get { return FontSize; } set { FontSize = value; } } // Создаем виртуальный метод public virtual string FontInfo(Font obj) { string s = "Информация о шрифте: \n------------------\n\n" + "Тип шрифта: " + typeFont + "\nРазмер шрифта: " + fontSize + "\n"; return s; } } // Производный класс 1 уровня class ColorFont: Font { byte Color; public ColorFont(byte Color, string TypeFont, short FontSize) : base(TypeFont, FontSize) { this.Color = Color; } // Переопределение для виртуального метода public override string FontInfo(Font obj) { // Используется ссылка на метод, определенный в базовом классе Font return base.FontInfo(obj) + "Цвет шрифта: " + Color + "\n"; } // Создадим виртуальное свойство public virtual byte color { set { Color = value; } get { return Color; } } } // Производный класс 2 уровня class GradientColorFont: ColorFont { char TypeGradient; public GradientColorFont(char TypeGradient, byte Color, string TypeFont, short FontSize) : base(Color, TypeFont, FontSize) { this.TypeGradient = TypeGradient; } // Опять переопределяем виртуальный метод public override string FontInfo(Font obj) { // Используется ссылка на метод определенный в производном классе FontColor return base.FontInfo(obj) + "Тип градиента: " + TypeGradient + "\n\n"; } // Переопределим виртуальное свойство public override byte color { get { return base.color; } set { if (value

Начну с повторения: вы не должны вызывать виртуальные функции во время работы конструкторов или деструкторов, потому что эти вызовы будут делать не то, что вы думаете, и результатами их работы вы будете недовольны. Если вы – программист на Java или C#, то обратите на это правило особое внимание, потому что это в этом отношении C++ ведет себя иначе.

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


class Transaction { // базовый класс для всех

public: // транзакций

virtual void logTransaction() const = 0; // выполняет зависящую от типа

// запись в протокол

Transaction::Transaction() // реализация конструктора

{ // базового класса

logTransaction();

class BuyTransaction: public Transaction { // производный класс

// транзакции данного типа

class SellTransaction: public Transaction { // производный класс

virtual void logTransaction() const = 0; // как протоколировать

// транзакции данного типа


Посмотрим, что произойдет при исполнении следующего кода:


BuyTransaction b;


Ясно, что будет вызван конструктор BuyTransaction, но сначала должен быть вызван конструктор Transaction, потому что части объекта, принадлежащие базовому классу, конструируются прежде, чем части, принадлежащие производному классу. В последней строке конструктора Transaction вызывается виртуальная функция logTransaction, тут-то и начинаются сюрпризы. Здесь вызывается та версия logTransaction, которая определена в классе Transaction, а не в BuyTransaction, несмотря на то что тип создаваемого объекта – BuyTransaction. Во время конструирования базового класса не вызываются виртуальные функции, определенные в производном классе. Объект ведет себя так, как будто он принадлежит базовому типу. Короче говоря, во время конструирования базового класса виртуальных функций не существует.

Есть веская причина для столь, казалось бы, неожиданного поведения. Поскольку конструкторы базовых классов вызываются раньше, чем конструкторы производных, то данные-члены производного класса еще не инициализированы во время работы конструктора базового класса. Это может стать причиной неопределенного поведения и близкого знакомства с отладчиком. Обращение к тем частям объекта, которые еще не были инициализированы, опасно, поэтому C++ не дает такой возможности.

Есть даже более фундаментальные причины. Пока над созданием объекта производного класса трудится конструктор базового класса, типом объекта является базовый класс. Не только виртуальные функции считают его таковым, но и все прочие механизмы языка, использующие информацию о типе во время исполнения (например, описанный в правиле 27 оператор dynamic_cast и оператор typeid). В нашем примере, пока работает конструктор Transaction, инициализируя базовую часть объекта BuyTransaction, этот объект относится к типу Transaction. Именно так его воспринимают все части C++, и в этом есть смысл: части объекта, относящиеся к BuyTransaction, еще не инициализированы, поэтому безопаснее считать, что их не существует вовсе. Объект не является объектом производного класса до тех пор, пока не начнется исполнение конструктора последнего.

То же относится и к деструкторам. Как только начинает исполнение деструктор производного класса, предполагается, что данные-члены, принадлежащие этому классу, не определены, поэтому C++ считает, что их больше не существует. При входе в деструктор базового класса наш объект становится объектом базового класса, и все части C++ – виртуальные функции, оператор dynamic_cast и т. п. – воспринимают его именно так.

В приведенном выше примере кода конструктор Transaction напрямую обращается к виртуальной функции, что представляет собой откровенное нарушение принципов, описанных в данном правиле. Это нарушение легко обнаружить, поэтому некоторые компиляторы выдают предупреждение (а другие – нет; дискуссию о предупреждениях см. в правиле 53). Но даже без такого предупреждения ошибка наверняка проявится до времени исполнения, потому что функция logTransaction в классе Transaction объявлена чисто виртуальной. Если только она не была где-то определена (маловероятно, но возможно – см. правило 34), то такая программа не скомпонуется: компоновщик не найдет необходимую реализацию Transaction::logTransaction.

Не всегда так просто обнаружить вызов виртуальной функции во время работы конструктора или деструктора. Если Transaction имеет несколько конструкторов, каждый из которых выполняет одну и ту же работу, то следует проектировать программу так, чтобы избежать дублирования кода, поместив общую часть инициализации, включая вызов logTransaction, в закрытую невиртуальную функцию инициализации, скажем, init:


class Transaction {

{ init(); } // вызов невиртуальной функции

Virtual void logTransaction() const = 0;

logTransaction(); // а это вызов виртуальной

// функции!


Концептуально этот код не отличается от приведенного выше, но он более коварный, потому что обычно будет скомпилирован и скомпонован без предупреждений. В этом случае, поскольку logTransaction – чисто виртуальная функция класса Transaction, в момент ее вызова большинство систем времени исполнения прервут программу (обычно выдав соответствующее сообщение). Однако если logTransaction будет «нормальной» виртуальной функцией, у которой в классе Transaction есть реализация, то эта функция и будет вызвана, и программа радостно продолжит работу, оставляя вас в недоумении, почему при создании объекта производного класса была вызвана неверная версия logTransaction. Единственный способ избежать этой проблемы – убедиться, что ни один из конструкторов и деструкторов не вызывает виртуальных функций при создании или уничтожении объекта, и что все функции, к которым они обращаются, следуют тому же правилу.

Но как вы можете убедиться в том, что вызывается правильная версия log-Transaction при создании любого объекта из иерархии Transaction? Понятно, что вызов виртуальной функции объекта из конструкторов не годится.

Есть разные варианты решения этой проблемы. Один из них – сделать функцию logTransaction невиртуальной в классе Transaction, затем потребовать, чтобы конструкторы производного класса передавали необходимую для записи в протокол информацию конструктору Transaction. Эта функция затем могла бы безопасно вызвать невиртуальную logTransaction. Примерно так:


class Transaction {

explicit Transaction(const std::string& loginfo);

void logTransaction(const std::string& loginfo) const; // теперь –

// невиртуальная

// функция

Transaction::Transaction(const std::string& loginfo)

logTransaction(loginfo); // теперь –

// невиртуальный

class BuyTransaction: public Transaction {

BuyTransaction(parameters )

: Transaction(createLogString(parameters )) // передать информацию

{...} // для записи в протокол

... // конструктору базового

static std::string createLogString(parameters );


Другими словами, если вы не можете вызывать виртуальные функции из конструктора базового класса, то можете компенсировать это передачей необходимой информации конструктору базового класса из конструктора производного.

В этом примере обратите внимание на применение закрытой статической функции createLogString в BuyTransaction. Использование вспомогательной функции для создания значения, передаваемого конструктору базового класса, часто удобнее (и лучше читается), чем отслеживание длинного списка инициализации членов для передачи базовому классу того, что ему нужно. Сделав эту функцию статической, мы избегаем опасности нечаянно сослаться на неинициализированные данные-члены класса BuyTransaction. Это важно, поскольку тот факт, что эти данные-члены еще не определены, и является основной причиной, почему нельзя вызывать виртуальные функции из конструкторов и деструкторов.

Виртуальная функция

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

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

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

Пример

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

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

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

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

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

Объявим два класса. Предка(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 () ; // this will print the text "Ancestor::function1" } } ;

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

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

См. также

Ссылки

  • C++ FAQ Lite: Виртуальные функции в C++ (англ.)

Wikimedia Foundation . 2010 .