Продолжим обсуждение виртуальных функций, начатый в предыдущей работе. Когда говорят о вызове виртуальных функций, обычно имеют в виду т.н. позднее связывание объекта производного класса с этой функцией. При обычном связывании через заранее созданный объект класса адрес функции известен на стадии компиляции. При позднем связывании адреса виртуальных функций заносятся в создаваемую для каждого класса при выполнении программы таблицу vftable, поэтому адрес функции в одной и той же точке программы может быть различным в зависимости от значения указателя, обращающегося к таблице. Т.е. конкретная реализация виртуальной функции определяется динамически во время выполнения программы.
Указателем, обращающимся к таблице, является указатель на базовый класс. Его значением может быть адрес объекта любого класса иерархии.
Очень часто класс, содержащей виртуальный метод, называют полиморфным классом. Главная особенность заключается в том, что полиморфные классы допускают обработку объектов, тип которых неизвестен во время компиляции. Если в производном классе виртуальный метод не будет переопределен, то при вызове будет найден метод с таким именем вверх по иерархии классов (т.е. в базовом классе). Схема использования виртуальных функций приведена ниже.
class Based {
public:
virtual int fb(){return 0;}
//При наличии в классе виртуальных функций необходим виртуальный деструктор
virtual ~Based(){}
};
class Derived1:public Based {
public:
int fb() {return 5;}
~Derived1(){}
};
class Derived2:public Based {
int fb() {return 7;}
~Derived2(){}
};
class Derived3:public Based {
// fb не определена
};
void main()
{
Derived1 d1;
cout<< d1.fb(); //Позднее связывание не работает
//Правильный вызов
Based *pb=0;
pb=new Derived1;
cout<<pb->fb(); // возвращает 5
delete pb;
pb=new Derived2;
cout<<pb->fb(); // возвращает 7
delete pb;
pb=new Derived3;
cout<<pb->fb(); // возвращает 0
if(pb) delete pb;
}
В лабораторной работе для изучения механизма работы виртуальных функций используется идея паттерна проектирования программ под названием «Фабричный метод» (Faсtory Method) [3]. Паттерн используется в тех случаях, когда выбор класса, объект которого надо создавать, делается в процессе выполнения программы.
Здесь он применяется в программе, работающей с объектами двух разных классов T1 и T2, которые являются производными классами одного базового класса Shape.
Производные классы должны создавать плоские объекты следующих типов: квадрат, треугольник, прямоугольник, параллелограмм, трапеция, правильный шестиугольник, правильный восьмиугольник (square, triangle, rectangle, parallelogram, trapeze, hexagon, octagon). Для каждого типа фигуры вычисляются площадь, центр тяжести, радиусы вписанных или описанных окружностей и другие атрибуты [5], а также должны быть предусмотрены виртуальные методы Вращения (Rotate) и Перемещения (Move).
Операции над объектами этих классов могут быть такими (функции с 2-мя аргументами Shape* obj1, Shape* obj2):
- сравнить два объекта по площади - Compare;
- определить факт пересечения объектов – IsIntersect;
- определить факт включения одного объекта в другой – IsInclude.
Алгоритмы для реализации этих функций есть в любом учебнике по компьютерной графике. Если вас затрудняет реализация этих алгоритмов, то я разрешаю вместо них сделать следующее. Введем область вращения фигуры, представляющую из себя окружность, описанную вокруг нее, там, где это возможно, или проведенную из центра тяжести с радиусом, равным расстоянию от него до наиболее удаленной точки фигуры. Теперь перечисленные операции можно реализовывать не с объектами, а с их областями вращения.
Ниже приводится рекомендуемый вариант программы (упрощенный). В ней из трех фигур выбираются и обрабатываются две – объекты Triangle и Rectangle.
#include "stdafx.h"
#include <iostream>
using namespace std;
class Point{
public:int x,y;
};
class Shape { // Абстрактный базовый класс
public:
Point *arc;
char ID;
/* Поля и методы */
virtual int GetArea()=0;//Вычисление площади фигуры
int GetArea(){/*вычисление конкретной площади*/return 3;}
~Triangle(){delete[] arc;}
};
class Octagon : public Shape {
public:
Octagon(){arc=new Point[8]; ID='O';}
void CreateShape(){/*...*/}
/* .....*/
int GetArea(){/*вычисление конкретной площади*/return 8;};
~Octagon(){delete[] arc;}
};
class Operation
{ //Класс, инкапсулирующий методы обработки объектов классов
public:
void Compare(Shape* s1, Shape* s2)
{// Проверка правильности подбора типов фигур
if ((s1->ID=='T' && s2->ID=='R')||(s2->ID=='T' && s1->ID=='R'))
cout<<"Correct choice"<<endl;
else {cout<<"Not such operation"<<endl; return;}
//Вычисления
if (s1->GetArea()>=s2->GetArea()) cout<<"Area "<<s1->ID<<" >= "<<s2->ID<<endl;
else cout<<"Area "<<s1->ID<<" < "<<s2->ID<<endl;
}
void IsInclude(Shape* s1, Shape* s2)
{/* */}
void IsIntersect(Shape* s1, Shape* s2)
{/* */}
};
class FactoryShape{// Класс–фабрика объектов, производных от Shape
public:
Shape* generator()
{ switch(rand() % 3) {
case 0:
return new Triangle;
case 1:
return new Rectangle;
case 2:
return new Octagon;
}
}
};
int _tmain(int argc, _TCHAR* argv[])
{
//Создаем 2 указателя на базовый класс
Shape *p1=0,*p2=0;
//Создаем заданные объекты
FactoryShape f;
cout<<"Input 1-t shape"<<endl;
char cond='y';
while (cond=='y')
{
if (p1) delete p1;
p1=f.generator();
cout<<p1->ID<<endl;
cout<<"Continue choice?(y/…)"<<endl;
cin>>cond;
}
p1->CreateShape();
cout<<"Input 2-d shape"<<endl;
cond='y';
while (cond=='y')
{if (p2) delete p2;
p2=f.generator();
cout<<p2->ID<<endl;
cout<<"Continue choice? (y/…)"<<endl;
cin>>cond;
}
p2->CreateShape();
Operation op;
op.Compare(p1,p2); //Сравниваем площади
if(p1) delete p1;
if(p2) delete p2;
return 0;
}
Замечание. Можно для идентификации класса текущего объекта воспользоваться библиотекой typeinfo (#include <typeinfo>) и его компонентой typeid. Название класса выводится
typeid(*p).name(), где p=указатель на базовый класс.
Итак, конкретное задание подразумевает создание фабрики объектов для всех 7 фигур, выбором 2-х из них согласно заданию и написание для них функций класса Operation.
В этой работе я не требую вводить какие-либо классы для упрощения main, так как главное в ней – разобраться с механизмом вызова виртуальных функций.
Варианты заданий
Варианты заданий приведены в следующей таблице:
№ T1 T2
1. Треугольник Квадрат
2. Треугольник Прямоугольник
3. Треугольник Параллелограмм
4. Треугольник Трапеция
5. Треугольник Шестиугольник
6. Треугольник Восьмиугольник
7. Квадрат Прямоугольник
8. Квадрат Параллелограмм
9. Квадрат Трапеция
10. Квадрат Шестиугольник
11. Квадрат Восьмиугольник
12. Прямоугольник Параллелограм
13. Прямоугольник Трапеция
14. Прямоугольник Шестиугольник
15. Прямоугольник Восьмиугольник
16. Параллелограмм Трапеция
17. Параллелограмм Шестиугольник
18. Параллелограмм Восьмиугольник
19. Трапеция Шестиугольник
20. Трапеция Восьмиугольник
21. Шестиугольник Восьмиугольник
Контрольные вопросы
1. Различают 4 элемента определения функции: тип, имя, список параметров, тело. Какими элементами могут отличаться экземпляры одной виртуальной функции, находящиеся в разных производных классах? А в переопределенной функции внутри одного класса?
2. Что изменится в поведении вышеприведенной программы, если в базовом классе заменить строку
virtual int GetArea()=0
строкой
int GetArea(){}? Покажите эти изменения, используя режим отладки.
3. При компиляции вышеприведенной программы появляется 1 предупреждение. Чего хочет компилятор?
4. В каких случаях применяется фабрика объектов?
5. В нижеследующей программе выполните правильное обращение к функции speak производного класса, для чего замените одиночные знаки «?» корректными символами или вообще уберите их, а сдвоенные знаки «??» замените именами классов.