avangard-pressa.ru

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

Концепция полиморфизма является очень важной в ООП. Этот термин, как показано выше, используется для описания процесса, при котором различ­ные реализации функции могут быть доступны с использованием одного имени. В С++ полиморфизм поддерживается и во время компиляции, и во время испол­нения программы. Рассмотренные выше перегрузки функций и операций — это примеры полиморфизма во время компиляции.

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

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

1. Имеются отличия в типах параметров в объявлении функции, напри­мер, при перегрузке функций – Show(int, char) и Show(int, *char) не одно и то же.

2. Задана операция доступа к области действия, поэтому Circle::Show отличается от Point::Show и ::Show.

3. Объект класса идентифицирует метод: Acircle.Show вызывает метод Circle::Show, а Apoint.Show – Point::Show. Аналогично и в случае указа­телей на объекты: pointptr->Show инициирует Point::Show.

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

Стандартная графическая библиотека предоставляет в распоряжение пользователя объявления классов в соответствующих исходных h-файлах и ме­тоды в объектных obj-файлах или библиотечных lib-файлах. Накладываемые при этом ограничения раннего связывания не позволяют пользователю легко до­бавлять новые классы. С++ предоставляет гибкий механизм для решения этих проблем с помощью специальных методов, называемых виртуальными функция­ми.

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

Для простоты можно сказать, что виртуальная функция — это функция, вызов которой зависит от типа объекта. На практике это означает, что решение о том, какую функцию Show вызывать, откладывается до момента выявления типа объекта на стадии исполнения. В языке С можно передать объект данных функции, при этом необходимо было задать его тип, когда писалась программа. В ООП можно писать виртуальные функции так, чтобы объект сам определял, какую функцию необходимо вызвать во время исполнения программы. Это до­стигается использованием указателей на базовые типы и виртуальных функций.

Указатели на производные типы.

Для лучшего понимания виртуальных функций важно понимать принцип взаимодействия классов в С++, связанных отношением наследования — указа­тели на базовый тип и на производный тип зависимы: указатель на базовый класс может ссылаться на объект этого класса или любой объект производного класса, но указатель производного класса не может ссылаться на объекты базового класса:

Объект базового класса

Указатель на базовый класс Объект производного Указатель на

класса производный

Объект производного класс

класса

Пусть есть цепь наследования классов: А <- B <- C. В программе объекты классов могут объявляться так:

A obA; B obB; C obC;

A* p

;

// объявлен указатель на базовый класс типаA

*

p = &obB; // указателю р присвоен адрес объекта obB

Объект obB можно представлять как особый вид объекта класса А (так зо­лотая рыбка — особая разновидность рыб, но это рыба). Но объект типа А не является особым видом объектов классов В и С. Например, вы обладаете мно­гими чертами ваших родителей, наследуемых от них и их родителей (цвет во­лос, глаз). Ваши родители и их родители – часть вас, но вы не являетесь их ча­стью.

Все элементы класса С, наследуемые от классов А и В, могут быть до­ступны через использование указателя р. Однако на элементы, объявленные (собственные) в классе С нельзя ссылаться, используя р. Если требуется иметь доступ к элементам, объявленным в производном классе, используя указатель на базовый класс, его надо привести к указателю на производный класс так:

(( С*)р) -> f(), где функция f() является элементом класса С, причем внешние скобки необходимы. И, наконец, указатель р изменяется при операци­ях ++ и -- относительно базового класса.

Пример 31.

Рассмотрим программу использования указателей на базовый класс из производных классов по цепи наследования: Base <- Derive <- Derive1.

#include

#include

#include

class Base // базовый класс Base

{ public:

void show(void)

{ cout<<"In Base class\n"; }

};

class Derive: public Base // производный класс от Base

{ public:

void show(void)

{ cout<<"In Derive class\n"; }

};

class Derive1: public Derive // производный класс от Base, Derive

{ public:

void show(void)

{ cout<<"In Derive1 class\n"; }

};

void main() // главная функция

{ clrscr();

Base bobj, *pb; // объект и указатель базового класса

Derive dobj, *pd; // объекты и указатели производных классов

Derive1 d1obj, *pd1;

pb=&bobj; // указатель на объект класса Base

bobj.show(); // вызов объектом функции show() класса Base

pb->show(); // вызов указателем функции show() класса Base

pd=&dobj; // указатель на объект класса Derive

dobj.show(); // вызов объектом функции show() класса Derive

pd->show(); // вызов указателем функции show() класса Derive

pd1=&d1obj; // указатель на объект класса Derive1

d1obj.show(); // вызов объектом функции show() класса Derive1

pd1->show(); // вызов указателем функции show() класса Derive1

pb=&dobj; // базовый указатель на объект класса Derive

pb->show(); // вызов указателем функции show() класса Base !

pb=&d1obj; // базовый указатель на объект класса Derive1

pb->show(); // вызов указателем функции show() класса Base !

((Derive1 *)pb)->show(); // вызов show() класса Derive1 указателем Base,

// приведенным к типу Derive1

getch();

}

Результаты программы:

In Base class

In Base class

In Derive class

In Derive class

In Derive1 class

In Derive1 class

In Base class !

In Base class !

In Derive1 class

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

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

Виртуальная функция должна быть элементом класса (она не может иметь спецификатор friend). Однако виртуальная функция может быть "другом" другого класса. Допустимо, чтобы деструктор имел спецификатор virtual, од­нако для конструктора это запрещено. Вследствие запретов и различий между перегрузкой обычных функций и переопределением виртуальных функций для них часто используется термин "замещение" (overriding).

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

Пример 32.

Модифицируем программу примера 31 так, что в классе Base объявим функцию show() как виртуальную:

class Base // базовый класс Base

{ public:

virtual void show(void) // виртуальная функция класса

{ cout<<"In Base class\n";}

};

В главной функции исключим операторы вызова объектом функции show() (например, bobj.show(); и др.) и оставим вызовы функции show() толь­ко с помощью указателей (pb->show(); и др.).

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

In Base class

In Derive class

In Derive1 class

In Derive class

In Derive1 class

Особенность использования виртуальных функций — при вызове функ­ции, объявленной виртуальной, определяется версия функции в зависимости от того, на объект какого класса будет ссылаться указатель базового класса.

Базовый класс задает основной интерфейс виртуальных функций, кото­рый будут иметь производные классы. Но производные классы задают свой ме­тод согласно принципу полиморфизма: "один интерфейс – разные методы". Отделение интерфейса и реализации функций позволяет создавать библиотеки классов (class libraries).

Возникает вопрос: как осуществляется вызов виртуальной функции для активного объекта? Реализации компиляторов пользуются технологией преоб­разования имени виртуальной функции в индекс в таблице, содержащей указа­тели на функции, называемой "таблицей виртуальных функций" (virtual function table — vtbl). Каждый класс с виртуальными функциями имеет свою vtbl, иден­тифицирующую его виртуальные функции. Это можно изобразить схематиче­ски:

Объект класса Base: vtbl:

*pf1 f1() *pf2 f2()

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

Пример 33.

Рассмотрим программу использования виртуальных функций при расши­ряющемся наследовании. Базовый класс Figure описывает плоскую фигуру для вычисления площади, которой достаточно двух измерений. Виртуальная функ­ция show_area() печатает значение площади фигуры. На основе этого класса создаются производные классы – Triangle (треугольник), Rectangle (прямо­угольник), Circle (круг с одним измерением), в которых определены конкретные формулы (методы) вычисления площадей. Классы связаны расширяющимся на­следованием по схеме: Figure <- (Triangle, Rectangle, Circle).

#include

#include

class Figure // базовый класс

{ protected: // защищенные элементы, доступные в производных классах

double x, y;

public:

void set_dim (double i, double j=0) // 2-й параметр по умолчанию

{ x = i, y = j; } // задание измерений фигур

virtual void show_area() // виртуальная функция Figure

{ cout<<"Площадь не определена для Figure\n";}

};

class Triangle: public Figure // производный класс

{ public:

void show_area() // виртуальная функция Triangle

{ cout<<"Треугольник с высотой "<< x <<" и основанием "<< y;

cout<<" имеет площадь = "<< x*0.5*y <<"\n";

}

};

class Rectangle: public Figure // производный класс

{ public:

void show_area() // виртуальная функция Rectangle

{ cout<<"Прямогольник со сторонами "<< x <<" и "<< y;

cout<<" имеет площадь = "<< x*y <<"\n";

}

};

class Circle: public Figure // производный класс

{ public:

void show_area() // виртуальная функция Circle

{ cout<<"Круг с радиусом "<< x;

cout<<" имеет площадь = "<< 3.14*x*x <<"\n";

}

};

void main ()

{ clrscr();

Figure f, *p; // объявление объекта и указателя класса Figure

Triangle t; // объявление объекта класса Triangle

Rectangle r; // объявление объекта класса Rectangle

Circle c; // объявление объекта класса Circle

p = &f; // указатель базового типа Figure

p -> set_dim (1,2); // задание размерностей объекта Figure

p -> show_area(); // вывод сообщения о площади объекта типа Figure

p = &t; // базовый указатель на объект типа Triangle

p -> set_dim (3,4); // задание размерностей объекта типа Triangle

p -> show_area(); // вывод сообщения о площади объекта типа Triangle

p = &r; // базовый указатель на объект типа Rectangle

p -> set_dim (5,6); // задание размерностей объекта типа Rectangle

p -> show_area(); // вывод сообщения о площади объекта типа Rectangle

p = &c; // базовый указатель на объект типа Circle

p -> set_dim (2); // задание размерностей объекта типа Circle

p -> show_area(); // вывод сообщения о площади объекта типа Circle

getch(); // задержка экрана результатов

}

Результаты программы:

Треугольник с высотой 3 и основанием 4 имеет площадь = 6

Прямогольник со сторонами 5 и 6 имеет площадь = 30

Круг с радиусом 2 имеет площадь = 12.56

Пример 34.

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

#include

#include

class Value // базовый класс

{ protected:

int value;

public:

Value (int n) { value = n; } // конструктор с параметром

virtual int getvalue () { return value; } // виртуальная функция

};

class Mult: public Value // производный класс

{ protected:

int mult;

public:

Mult (int n, int m): Value (n) // конструктор производного класса

{ mult = m;}

int getvalue () { return value * mult;}

};

void main ()

{ clrscr ();

cout<<"Работа программы";

Value *basep; // указатель базового типа

basep = new Value (10); // дин-ое создание объекта класса Value

cout<<"Для базового класса Value число = "

delete basep; // освобождение динамической памяти объекта Value

basep = new Mult (10, 2); // базовый указатель на объект типа Mult

cout<<"Для производ. класса Mult число = "

delete basep; //освобождение динамической памяти объекта типа Mult

}

Результаты программы:

Работа программы

Для базового класса Value число = 10

Для производ. класса Mult число = 20

Обычные или виртуальные функции.

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

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