Виртуальные функции. Полиморфизм

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

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

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

Рассмотрим теперь, как ведут себя при наследовании невиртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.

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

// BASE.DIR - определения базового и производного классовclass base { public: void fun (int i) { printf("\nbase::i =",i); }};class dir: public base{ public: void fun (int i) { printf("\nbase::i =",i); }};

В данном случае внешне одинаковые функции void fun(int) определены в базовом классе base и в производном классе dir.

В теле класса dir обращение к функции fun(), принадлежащей классу base, может быть выполнено с помощью полного квалифицированного имени, явно включающего имя базового класса: base::fun(). При обращении в классе dir к такой же (по внешнему виду) функции, принадлежащей классу dir, достаточно использовать имя fun() без предшествующего квалификатора.

В программе, где определены и доступны оба класса base и dir, обращения к функциям fun () могут быть выполнены с помощью указателей на объекты соответствующих классов:

// одинаковые функции в базовом и производном классах#include <stdio.h>#include “base.dir” // Определения классовvoid main (void){ base B, *bp = &B; dir D, *dp = &D; base *pbd = &D; bp->fun (1); // Печатает : base::i = 1 dp->fun (5); // Печатает : dir::i = 5 pbd->fun (4); // Печатает : base::i = 4}

В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес объекта производного класса (объекта D) присваивается указателю на объект его прямого базового класса (base*). При этом выполняется стандартное преобразование указателей, предусмотренное синтаксисом языка Си++. Обратное образование, т.е. преобразование указателя на объект базового класса в указатель на объект производного класса, невозможно (запрещено синтаксисом). Обращения к функциям классов base и dir с помощью указателей bp и dp не представляют особого интереса. Вызов pbd->fun() требуется прокомментировать. Указатель pbd имеет тип base*, однако его значение - адрес объекта D класса dir.

Какая же из функций base::fun() или dir::fun() вызывает при обращении pbd->fun()? Результат выполнения программы показывает, что вызывается функция из базового класса. Именно такой вызов предусмотрен синтаксисом языка Си++, т.е. выбор функции (не виртуальной) зависит только от типа указателя, но не от его значения. “Настроив” указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса.

Пусть в производном классе определена компонентная функция void show(). Доступ к функции show() производного класса возможен только с помощью явного указания области видимости:

имя_производного_класса::show(),

либо с использованием имени конкретного объекта:

имя_объекта_производного_класса.show().

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

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

00 //виртуальные функции в базовом и производном классах01 #include <stdio.h>02 class base 03 { public:04 virtual void vfun(int i) { printf("\nbase::i =",i); }05 };06 class dir1: public base 07 { public:08 void vfun(int i) { printf("\ndir1::i =",i); }09 };10 class dir2: public base 11 { public:12 void vfun(int i) { printf("\ndir2::i =",i); }13 };14 void main (void)15 {16 base B, *bp = &B;17 dir1 D1, *dp1 = &D1;18 dir2 D2, *dp2 = &D2;19 bp->vfun(1); // Печатает : base::i = 120 dp1->vfun(2); // Печатает : dir1::i = 221 dp2->vfun(3); // Печатает : dir2::i = 322 bp =&D1; bp->vfun(4); // Печатает : dir1::i = 423 bp =&D2; bp->vfun(5); // Печатает : dir2::i = 524 }

Заметим, что доступ к функциям vfun()организован через указатель bp на базовый класс. Когда он принимает значение адреса объекта базового класса, то вызывается функция из базового класса. Когда указателю присваиваются значения ссылок на объекты производных классов &D1, &D2, выбор соответствующего экземпляра функции определяется именно объектом. Таким образом, интерпретация каждого вызова виртуальной функции через указатель на базовый класс зависит от значения этого указателя, то есть от типа объекта, для которого выполняется вызов. Для невиртуальной функции ее вызов через указатель интерпретируется взависимости от типа указателя.

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

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

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

00 // особенности виртуальных функций01 #include <stdio.h>02 class base 03 { public:04 virtual void f1(void){ printf("\nbase::f1"); }05 virtual void f2(void){ printf("\nbase::f2"); }06 virtual void f3(void){ printf("\nbase::f3"); }07 };08 class dir: public base 09 { public: 10 void f1(void){ printf("\ndir::f1"); } // виртуальная11 //int f2(void){ printf("\ndir::f2"); } // ошибка в типе12 void f3(int i){printf("\ndir::f3:%d",i);} //невиртуальная13 };14 void main (void)15 {16 base B, *bp = &B; dir D, *dp = &D;17 bp->f1(); // Печатает : base::f118 bp->f2(); // Печатает : base::f219 bp->f3(); // Печатает : base::f320 dp->f1(); // Печатает : dir::f121 dp->f2(); // Печатает : base::f222 //dp->f3(); // Не печатает - вызов без параметра23 dp->f3(3); // Печатает : dir::f3::324 bp = &D;25 bp->f1(); // Печатает : dir::f126 bp->f2(); // Печатает : base::f227 bp->f3(); // Печатает : base::f328 //bp->f3(3); // Не печатает - лишний параметр29 }

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

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

class base{ public: virtual int f (int j) { return j * j; }};class dir: public base { public: int f (int i){ return base::f (i * 2); } };

Абстрактные классы

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

virtual тип имя_функции(список_формальных_параметров) = 0;

В этой записи конструкция “= 0” называется “чистый спецификатор”. Пример описания чистой виртуальной функции:

virtual void fpure(void) = 0;

Чистая виртуальная функция “ничего не делает” и недоступна для вызовов. Ее назначение - служить основой для подменяющих ее функций в производных классах. Исходя из этого становится понятной невозможность создания самостоятельных объектов абстрактного класса. Абстрактный класс может использоваться только в качестве базового для производных классов. При создании объектов такого производного класса в качестве подобъектов создаются объекты базового абстрактного класса. Пример:

class B { protected: virtual void f(int) = 0; void s(int);};class D: public B { . . . void f(int); };class E: public B { . . . void s(int); };

Здесь B - абстрактный, D - нет, поскольку f - переопределена, а s - наследуется, E - абстрактный, так как s - переопределена, а f - наследуется.

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

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

Преобразование типов

В большинстве случаев сработает традиционное *(type*)&x, однако у программиста должна быть полная уверенность в возможности такого преобразования. В самом общем случае следует использовать один из новых операторов.

dynamic_cast<T*>(argptr) // требуется RTTIstatic_cast<T>(arg) // выполняется на этапе компиляции