Здавалка
Главная | Обратная связь

Виртуальные функции.



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

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

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

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

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

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

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

Несмотря на то, что указатель point_to_Object имеет тип А*, а не С* (или В*), он может ссылаться на объекты типа С (или В). А теперь рассмотрим вариант неправильной записи:

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

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

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

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

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

Применение.

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

class Weapon{ public: ... //тут будут данные-члены, которыми может описываться, например, как //толщина дубины, так и количество гранат в гранатомете //эта часть для нас не важна virtual void Use1(void);//обычно - левая кнопка мыши virtual void Use2(void);//обычно - правая кнопка мыши ... //тут будут еще какие-то данные-члены и методы };

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

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

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

int TypeOfWeapon;

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

if("нажата левая кнопка мыши") Arms[TypeOfWeapon]->Use1();else Arms[TypeOfWeapon]->Use2();

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







©2015 arhivinfo.ru Все права принадлежат авторам размещенных материалов.