Void main (void)

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

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

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

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

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

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

//BASE.DIR - определения базового и производного классов

struct base {

void fun(int i) { cout << "\nbase::i = " << i; }

};

struct dir: public base{

void fun (int i){ cout << "\ndir::i - " << i; }

};

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

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

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

//Р10-05.СРР - одинаковыефункции в базовом ипроизводном

// классах

#include <iostream.h>

#include "base.dir" // Определения классов

{

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

}

Результаты выполнения программы:

base::i = 1 dir::i = 5 base::i = 4

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

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

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

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

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

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

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

Большую гибкость (особенно при использовании уже готовых би­блиотек классов) обеспечивает позднее (отложенное), или динамиче­ское связывание, которое предоставляется механизмом виртуальных функций. Любая нестатическая функция базового класса может быть сделана виртуальной, если в ее объявлении использовать специфика­тор virtual.Прежде чем объяснить преимущества динамического свя­зывания, приведем пример. Опишем в базовом классе виртуальную функцию и введем два производных класса, где определим функции с такими же прототипами, но без спецификатора virtual. Вследующей программе в базовом классе baseопределена виртуальная функция voidvfun(int). В производных классах dirl, dir2эта функция подменяется (override), т.е. определена по-другому:

//Р10-06.СРР - виртуальная функция в базовом классе

#include <iostream.h>

#include <conio.h>

struct base {

virtual void vfun(int i) { cout << "\nbase::i - " << i; }

};

struct dir1: public base {

void vfun (int i){ cout << "\ndirl::i - " << i; }

};

struct dir2: public base {

void vfun (int i){ cout << "\ndir2::i = " << i; }

};