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; }
};