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

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

Class A { public: virtual int Fun1(int); };

Один дополнительный спецификатор в объявлении функции и больше никаких (пока никаких) изменений в объявлениях производных классов. Как всегда, очень простая функция main(). В ней мы определяем указатель на объект базового класса, настраиваем его на объект производного типа, после чего по указателю мы вызываем функцию Fun1():

Void main () { A *pObj; A MyA; AB MyAB; pObj = &MyA; pObj->Fun1(1); AC MyAC; pObj = &MyAC; pObj->Fun1(1); }

Если бы не спецификатор virtual, результат выполнения выражения вызова

PObj->Fun1(1);

был бы очевиден: как известно, выбор функции определяется типом указателя.

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

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

//pObj->Fun2(2); //pObj->AC::Fun1(2);

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

Наличие этих самых таблиц виртуальных функций можно попытаться обнаружить с помощью операции sizeof. Конечно, здесь всё зависит от конкретной реализации, но, по крайней мере, в версии Borland C++ объект-представитель класса, содержащего объявления виртуальных функций, занимает больше памяти, нежели объект аналогичного класса, в котором те же самые функции объявлены без спецификатора virtual.

Cout << "Размеры объекта: " << sizeof(MyAC) << "…" << endl;

Так что объект производного класса приобретает дополнительный элемент - указатель на таблицу виртуальных функций. Схему такого объекта можно представить следующим образом (указатель на таблицу мы обозначим идентификатором vptr, таблицу виртуальных функций - идентификатором vtbl):

MyAC::= vptr A AC vtbl::= &AC::Fun1

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

PObj->Fun1(1);

можно представить следующим образом:

(*(pObj->vptr)) (pObj,1);

Здесь только на первый взгляд всё запутано и непонятно. На самом деле, в этом операторе нет ни одного не известного нам выражения.

Здесь буквально сказано следующее:

ВЫЗВАТЬ ФУНКЦИЮ, РАСПОЛОЖЕННУЮ ПО НУЛЕВОМУ ИНДЕКСУ ТАБЛИЦЫ ВИРТУАЛЬНЫХ ФУНКЦИЙ vtbl (в этой таблице у нас всего один элемент), АДРЕС НАЧАЛА КОТОРОЙ МОЖНО НАЙТИ ПО УКАЗАТЕЛЮ vptr.

В СВОЮ ОЧЕРЕДЬ, ЭТОТ УКАЗАТЕЛЬ ДОСТУПЕН ПО УКАЗАТЕЛЮ pObj, НАСТРОЕННОМУ НА ОБЪЕКТ MyAC. ФУНКЦИИ ПЕРЕДАЁТСЯ ДВА (!) ПАРАМЕТРА, ПЕРВЫЙ ИЗ КОТОРЫХ ЯВЛЯЕТСЯ АДРЕСОМ ОБЪЕКТА MyAC (значение для this указателя!), ВТОРОЙ - ЦЕЛОЧИСЛЕННЫМ ЗНАЧЕНИЕМ, РАВНЫМ 1.

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

PObj->A::Fun1(1);

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

Мы в очередной раз модифицируем объявление классов A, AB и объявляем новый класс ABC.

Модификация классов A и AB сводится к объявлению в них новых функций-членов:

Class A { public: virtual int Fun1(int key); virtual int Fun2(int key); }; ::::: int A::Fun2(int key) { cout << " Fun2(" << key << ") from A " << endl; return 0; } class AB: public A { public: int Fun1(int key); int Fun2(int key); }; ::::: int AB::Fun2(int key) { cout << " Fun2(" << key << ") from AB " << endl; return 0; } Класс ABC является производным от класса AB: class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC " << endl; return 0; }

В этот класс входит объявление функции-члена Fun1, которая объявляется в косвенном базовом классе A как виртуальная функция. Кроме того, этот класс наследует от непосредственной базы функцию-член Fun2. Эта функция также объявляется в базовом классе A как виртуальная. Мы объявляем объект-представитель класса ABC:

ABC MyABC;

Его схему можно представить следующим образом:

MyABC::= vptr A AB ABC vtbl::= &AB::Fun2 &ABC::Fun1

Таблица виртуальных функций сейчас содержит два элемента. Мы настраиваем указатель на объект базового класса на объект MyABC, затем вызываем функции-члены:

PObj = &MyABC; pObj->Fun1(1); pObj->Fun2(2);

В этом случае невозможно вызвать функцию-член AB::Fun1(), поскольку её адрес не содержится в списке виртуальных функций, а с верхнего уровня объекта MyABC, на который настроен указатель pObj, она просто не видна. Таблица виртуальных функций строится конструктором в момент создания объекта соответствующего объекта. Безусловно, транслятор обеспечивает соответствующее кодирование конструктора. Но транслятор не в состоянии определить содержание таблицы виртуальных функций для конкретного объекта. Это задача времени исполнения. Пока таблица виртуальных функций не будет построена для конкретного объекта, соответствующая функция-член производного класса не сможет быть вызвана. В этом легко убедиться, после очередной модификации объявления классов.

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

#include class A { public: virtual int Fun1(int key); }; int A::Fun1(int key) { cout << " Fun1(" << key << ") from A." << endl; return 0; } class AB: public A { public: AB() {Fun1(125);}; int Fun2(int key); }; int AB::Fun2(int key) { Fun1(key * 5); cout << " Fun2(" << key << ") from AB." << endl; return 0; } class ABC: public AB { public: int Fun1(int key); }; int ABC::Fun1(int key) { cout << " Fun1(" << key << ") from ABC." << endl; return 0; } void main () { ABC MyABC; // Вызывается A::Fun1(). MyABC.Fun1(1); // Вызывается ABC::Fun1(). MyABC.Fun2(1); // Вызываются AB::Fun2() и ABC::Fun1(). MyABC.A::Fun1(1); // Вызывается A::Fun1(). A *pObj = &MyABC; // Определяем и настраиваем указатель. cout << "==========" << endl; pObj->Fun1(2); // Вызывается ABC::Fun1(). //pObj->Fun2(2); // Эта функция через указатель недоступна!!! pObj->A::Fun1(2); // Вызывается A::Fun1(). }

Теперь в момент создания объекта MyABC

ABC MyABC;

из конструктора класса AB (а он вызывается раньше конструктора класса ABC), будет вызвана функция A::Fun1(). Эта функция является членом класса A. Объект MyABC ещё до конца не сформирован, таблица виртуальных функций ещё не заполнена, о существовании функции ABC::Fun1() ещё ничего не известно. После того, как объект MyABC будет окончательно сформирован, таблица виртуальных функций заполнится, а указатель pObj будет настроен на объект MyABC, вызов функции A::Fun1() через указатель pObj будет возможен лишь с использованием полного квалифицированного имени этой функции:

PObj->Fun1(1); // Это вызов функции ABC::Fun1()! pObj->A::Fun1(1); // Очевидно, что это вызов функции A::Fun1()!

Заметим, что вызов функции-члена Fun1 непосредственно из объекта MyABC приводит к аналогичному результату:

MyABC.Fun1(1); // Вызов функции ABC::Fun1().

А попытка вызова невиртуальной функции AB::Fun2() через указатель на объект базового класса заканчивается неудачей. В таблице виртуальных функций адреса этой функции нет, а с верхнего уровня объекта "посмотреть вниз" невозможно.

//pObj->Fun2(2); // Так нельзя!

Результат выполнения этой программки наглядно демонстрирует специфику использования виртуальных функций. Всего несколько строк…

Fun1(125) from A. Fun1(1) from ABC. Fun1(5) from ABC. Fun2(1) from AB. Fun1(1) from A. ========== Fun1(2) from ABC. Fun1(2) from A.

Один и тот же указатель в ходе выполнения программы может настраиваться на объекты-представители различных производных классов. В результате в буквальном смысле одно и то выражение вызова функции-члена обеспечивает выполнение совершенно разных функций. Впервые мы сталкиваемся с так называемым ПОЗДНИМ или ОТЛОЖЕННЫМ СВЯЗЫВАНИЕМ.

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

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

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

#include class A { public: void funA () {xFun();}; /*virtual*/void xFun () {cout <<"this is void A::xFun();"<< endl;}; }; class B: public A { public: void xFun () {cout <<"this is void B::xFun ();"<

В начале спецификатор virtual а определении функции A::xFun() закомментирован. Процесс выполнения программы состоит в определении объекта-представителя objB производного класса B и вызова для этого объекта функции-члена funA(). Эта функция наследуется из базового класса, она одна и очевидно, что её идентификация не вызывает у транслятора никаких проблем. Эта функция принадлежит базовому классу, а это означает, что в момент её вызова, управление передаётся "на верхний уровень" объекта objB. На этом же уровне располагается одна из функций с именем xFun(), и именно этой функции передаётся управление в ходе выполнения выражения вызова в теле функции funA(). Мало того, из функции funA() просто невозможно вызвать другую одноименную функцию. В момент разбора структуры класса A транслятор вообще не имеет никакого представления о структуре класса B. Функция xFun() - член класса B оказывается недостижима из функции funA().

Но если раскомментировать спецификатор virtual в определении функции A::xFun(), между двумя одноименными функциями установится отношение замещения, а порождение объекта objB будет сопровождаться созданием таблицы виртуальных функций, в соответствии с которой будет вызываться замещающая функция член класса B. Теперь для вызова замещаемой функции необходимо использовать её квалифицированное имя:

Void A::funA () { xFun(); A::xFun(); }

Чисто виртуальные функции

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

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

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

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

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

Методы(функции)

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

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

На этапе компиляции строится таблица виртуальных методов, а конкретный адрес проставляется уже на этапе выполнения.

При вызове метода с использованием указателя на класс действуют следующие правила:

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

В следующем примере иллюстрируется вызов виртуальных методов:

Class A // Объявление базового класса{ public: virtual void VirtMetod1(); // Виртуальный метод void Metod2(); // Не виртуальный метод};void A::VirtMetod() { cout << "Вызван A::VirtMetod1\n";} void A::Metod2() { cout << "Вызван A::Metod2\n"; } class B: public A // Объявление производного класса{public: void VirtMetod1(); // Виртуальный метод void Metod2(); // Не виртуальный метод};void B::VirtMetod1() { cout << "B::VirtMetod1\n";}void B::Metod2() { cout << "B::Metod2\n"; }void main() { B aB; // Объект класса B B *pB = &aB; // Указатель на объект класса B A *pA = &aB; // Указатель на объект класса A pA->VirtMetod1(); // Вызов метода VirtMetod класса B pB->VirtMetod1(); // Вызов метода VirtMetod класса B pA->Metod2(); // Вызов метода Metod2 класса A pB->Metod2(); // Вызов метода Metod2 класса B}

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

Вызван B::VirtMetod1Вызван B::VirtMetod1Вызван A::Metod2Вызван B::Metod2

Чисто виртуальной функцией называется виртуальная функция, указанная с инициализатором

Например:

Virtual void F1(int) =0;

Объявление класса может содержать виртуальный деструктор, используемый для удаления объекта определенного типа. Однако виртуального конструктора в языке С++ не существует. Некоторой альтернативой, позволяющей создавать объекты заданного типа, могут служить виртуальные методы, в которых выполняется вызов конструктора для создания объекта данного класса.

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

Виртуальная функция объявляется в базовом классе с использованием ключевого слова virtual. Когда же она переопределяется в производном классе, повторять ключевое слово virtual нет не­обходимости, хотя и в случае его повторного использования ошибки не возникнет.

В качестве первого примера виртуальной функции рассмотрим следующую короткую программу:

// небольшой пример использования виртуальных функций
#include
class Base {
public:

cout << *Base\n";
}
};

public:
void who() { // определение who() применительно к first_d
cout << "First derivation\n";
}
};
class seconded: public Base {
public:

cout << "Second derivation\n*";
}
};
int main()
{
Base base_obj;
Base *p;
first_d first_obj;
second_d second_obj;
p = &base_obj;
p->
p = &first_obj;
p->
p = &second_ob;
p->who(); // доступ к who класса second_d
return 0;
}

Программа выдаст следующий результат:

Base
First derivation
Second derivation

Проанализируем подробно эту программу, чтобы понять, как она работает.

Как можно видеть, в объекте Base функция who() объявлена как виртуальная. Это означает, что эта функция может быть переопределена в производных классах. В каждом из классов first_d и second_d функция who() переопределена. В функции main() определены три переменные. Первой является объект base_obj, имеющий тип Base. После этого объявлен указатель р на класс Base, затем объекты first_obj и second_obj, относящиеся к двум производным классам. Далее указателю р при­своен адрес объекта base_objи вызвана функция who(). Поскольку эта функция объявлена как виртуальная, то С++ определяет на этапе исполнения, какую из версий функции who() употребить, в зависимости от того, на какой объект указывает указатель р. В данном случае им является объект типа Base, поэтому исполняется версия функции who(), объявленная в классе Base. Затем указате­лю р присвоен адрес объекта first_obj. (Как известно, указатель на базовый класс может быть ис­пользован для любого производного класса.) После того, как функция who() была вызвана, С++ снова анализирует тип объекта, на который указывает р, для того, чтобы определить версию фун­кции who(), которую необходимо вызвать. Поскольку р указывает на объект типа first_d, то ис­пользуется соответствующая версия функции who(). Аналогично, когда указателю р присвоен адрес объекта second_obj, то используется версия функции who(), объявленная в классе second_d.

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

/* Здесь ссылка на базовый класс используется для доступа к виртуальной функции */
#include
class Base {
public:
virtual void who() { // определение виртуальной функции
cout << "Base\n";
}
};
class first_d: public Base {
public:
void who () { // определение who() применительно к first_d
cout << "First derivation\n";
}
};

public:
void who() { // определение who() применительно к second_d
cout << "Second derivation\n*";
}
};
// использование в качестве параметра ссылки на базовый класс
void show_who (Base &r) {
r.who();
}
int main()
{
Base base_obj;
first_d first_obj;
second_d second_obj;
show_who (base_ob j) ; // доступ к who класса Base
show_who(first_obj); // доступ к who класса first_d
show_who(second_obj); // доступ к who класса second_d
return 0;
}

Эта программа выводит на экран те же самые данные, что и предыдущая версия. В данном при­мере функция show_who() имеет параметр типа ссылки на класс Base. В функции main() вызов виртуальной функции осуществляется с использованием объектов типа Base, first_d и second_d. Вызываемая версия функции who() в функции show_who() определяется типом объекта, на кото­рый ссылается параметр при вызове функции.

Ключевым моментом в использовании виртуальной функции для обеспечения полиморфизма времени исполнения служит то, что используется указатель именно на базовый класс. Полимор­физм времени исполнения достигается только при вызове виртуальной функции с использовани­ем указателя или ссылки на базовый класс. Однако ничто не мешает вызывать виртуальные функ­ции, как и любые другие «нормальные» функции, однако достичь полиморфизма времени исполнения на этом пути не удается.

На первый взгляд переопределение виртуальной функции в производном классе выглядит как специальная форма перегрузки функции. Но это не так, и термин перегрузка функции не приме­ним к переопределению виртуальной функции, поскольку между ними имеются существенные раз­личия. Во-первых, функция должна соответствовать прототипу. Как известно, при перегрузке обычной функции число и тип параметров должны быть различными. Однако при переопределе­нии виртуальной функции интерфейс функции должен в точности соответствовать прототипу. Если же такого соответствия нет, то такая функция просто рассматривается как перегруженная и она утрачивает свои виртуальные свойства. Кроме того, если отличается только тип возвращаемо­го значения, то выдается сообщение об ошибке. (Функции, отличающиеся только типом возвра­щаемого значения, порождают неопределенность.) Другим ограничением является то, что вирту­альная функция должна быть членом, а не другом класса, для которого она определена. Тем не менее виртуальная функция может быть другом другого класса. Хотя деструктор может быть виртуальным, но конструктор виртуальным быть не может.

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

Если функция была объявлена как виртуальная, то она и остается таковой вне зависимости от количества уровней в иерархии классов, через которые она прошла. Например, если класс second_d получен из класса first_d, а не из класса Base, то функция who() останется виртуальной и будет вызываться корректная ее версия, как показано в следующем примере:

// порождение от first_d, а не от Base
class second_d: public first_d {
public:
void who() { // определение who() применительно к second_d
cout << "Second derivation\n*";
}
};

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

#include
class Base {
public:
virtual void who() {
cout << "Base\n";
}
};
class first_d: public Base {
public:
void who() {
cout << "First derivation\n";
}
};
class second_d: public Base {
// who() не определяется
};
int main()
{
Base base_obj;
Base *p;
first_d first_obj; ,
second_d second_obj;
p = &base_obj;
p->who(); // доступ к who класса Base
p = &first obj;
p->who(); // доступ к who класса first_d
p = &sepond_ob;
p->who(); /* доступ к who() класса Base, поскольку second_d не переопределяет */
return 0;
}

Эта программа выдаст следующий результат:

Base
First derivation
Base

Надо иметь в виду, что характеристики наследования носят иерархический характер. Чтобы проиллюстрировать это, предположим, что в предыдущем примере класс second_d порожден от класса first_d вместо класса Base. Когда функцию who() вызывают, используя указатель на объект типа second_d (в котором функция who() не определялась), то будет вызвана версия функции who(), объявленная в классе first_d, поскольку этот класс - ближайший к классу second_d. В общем случае, когда класс не переопределяет виртуальную функцию, С++ использует первое из определений, которое он находит, идя от потомков к предкам.

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

Полиморфизм предоставляет подклассу способ определения собственной версии метода, определенного в его базовом классе, с использованием процесса, который называется переопределением метода (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

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

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

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