На этом шаге мы рассмотрим поведение невиртуальных компонентных функций при наследовании.
К механизму виртуальных функций обращаются в тех случаях, когда в базовый класс необходимо поместить функцию, которая должна по-разному выполняться в производных классах. Точнее, по-разному должна выполняться не единственная функция из базового класса, а в каждом производном классе требуется свой вариант этой функции.
Например, базовый класс figure может описывать фигуру на экране без конкретизации ее вида, а производные классы (треугольник, эллипс и т.п.) однозначно определяют ее формы и размеры. Если в базовом классе ввести функцию для изображения фигуры на экране, то выполнение этой функции будет возможно только для объектов каждого из производных классов, определяющих конкретные изображения.
До объяснения возможностей виртуальных функций отметим, что классы, включающие такие функции, играют особую роль в объектно-ориентированном программировании. Именно поэтому они носят специальное название - полиморфные.
Рассмотрим теперь, как ведут себя при наследовании невиртуальные компонентные функции с одинаковыми именами, типами и сигнатурами параметров.
Если в базовом классе определена некоторая компонентная функция, то такая же функция (с тем же именем, того же типа и с тем же набором и типами параметров) может быть введена в производном классе. Рассмотрим следующее определение классов:
//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() могут быть выполнены с помощью указателей на объекты соответствующих классов:
//OOР29_1.СРР - одинаковые функции в базовом и производном классах. #include <iostream.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 }
base::i = 1 dir::i = 5 base::i = 4
В программе введены три указателя на объекты разных классов. Следует обратить внимание на инициализацию указателя pbd. В ней адрес объекта производного класса (объекта D) присваивается указателю на объект его прямого базового класса (base *). При этом выполняется стандартное преобразование указателей, предусмотренное синтаксисом языка C++. Обратное преобразование, т.е. преобразование указателя на объект базового класса в указатель на объект производного класса, невозможно (запрещено синтаксисом). Обращения к функциям классов base и dir с помощью указателей bp и dp не представляют особого интереса. Вызов pbd->fun() требуется прокомментировать. Указатель pbd имеет тип base*, однако его значение - адрес объекта D класса dir.
Какая же из функций base::fun() или dir::fun() вызывается при обращении pbd->fun()? Результат выполнения программы показывает, что вызывается функция из базового класса. Именно такой вызов предусмотрен синтаксисом языка C++, т.е. выбор функции (не виртуальной) зависит только от типа указателя, но не от его значения. "Настроив" указатель базового класса на объект производного класса, не удается с помощью этого указателя вызвать функцию из производного класса.
На следующем шаге мы рассмотрим этот же самый пример, но с использованием выртуальных функций.