В введенной графической иерархии тип каждого класса представляет какую-то геометрическую фигуру на экране: точку или окружность. Если потом возникнет желание заняться определением классов для отображения других структур, таких, например, как квадрат, треугольник, дуга и пр., то можно для каждой из них ввести соответствующие член – функции для отображения фигур на экране. Выражаясь в терминах принятых в ООП, можно сказать, что все эти типы графических фигур имеют возможность отображать себя на экране. При этом названные типы будут отличаться тем как они это делают. Так, точка рисуется с помощью процедуры, которой нужны только координаты X,Y и, возможно, цвет. Для окружности необходима более сложная процедура, которой помимо координаты центра нужен и радиус. Для дуги нужно еще указать угол и т. д. Те же вопросы возникают для стирания, перемещения по экрану и других основных манипуляций с фигурами на экране. Рассмотренные нами ранее член- функции классов позволяют решить эту задачу путем определения функции Show для каждого класса. Но такой подход имеет один существенный недостаток. Каждый раз, когда вводится новый класс, требуется вводить изменения и выполнять повторную компиляцию вследствие появления новой функции с именем Show. Это связано с тем , что заложенные в С++ механизмы определения того, какую функцию Show следует в настоящий момент использовать, позволяют решить эту задачу только одним из трех способов:
1. Имеются отличия в типах данных аргументов и объявлении функции, например, Show(int, char) не одно и то же, что Show(char*,font) .
2. Задана операция доступа к области действия, вследствие чего Circle::Show отличается от Point::Show и ::Show.
3. Объект класса идентифицирует функцию: ACircle.Show инициализирует Circle::Show, а APoint.Show- Point::Show. Аналогично и в случае указателей на объекты: FPoint_ptr->Show инициализирует Point::Show
Любой из этих способов позволяет определить требуемую функцию на этапе компиляции и поэтому носит название раннего или статического связывания.
Стандартная графическая библиотека предоставит в распоряжение пользователя объявления классов в соответствующих .h исходных заголовочных файлах и функции в объектных .OBJ или библиотечных .LIB файлах. Накладываемые при этом ограничения раннего связывания не позволяют пользователю легко добавлять новые образцы классов и даже у разработчиков могут возникнуть трудности при расширении пакета. С++ предоставляет гибкий механизм для решения этих проблем: позднее (или динамическое) связывание с помощью специальных методов, называемых виртуальными функциями.
Основная особенность обращения к виртуальным функциям состоит в том, что они разрешаются на этапе исполнения (отсюда термин – позднее связывание). На практике это означает, что решение, какую функцию Show вызывать, откладывается до момента выявления типа объекта на стадии исполнения . Виртуальная функция Show, спрятанная в классе B стандартной объектной библиотеки, не будет связана с объектами из B подобно тому, как это делается в случае обычных член- функций, принадлежащих B. Пользователю предоставляется возможность создать для своих нужд класс D, производный от В, в рамках которого определить свой собственный вариант Show. После выполнения компиляции полученный объектный модуль связывается объектной библиотекой. Вызовы функции Show как из функций класса В, так и из новых функций, реализованных в рамках D, автоматически разрешаются требуемым образом.
Выбор соответствующего варианта Show строится исключительно на определении типа объекта, осуществляющего обращение.
Пример на основе ранее определенной программы из файла Circle.cpp.
Рассмотрим функцию Circle::MoveTo из Circle.cpp.
void Circle::MoveTo(int NewX, int NewY)
{
Boolean vis=Visible;
if (vis) Hide(); //если имеется видимость – стираем
X=NewX; Y=NewY; // установка нового положения
if (vis) Show(); //рисование на новом месте, если есть
// видимость
}
Следует обратить внимание насколько это определение похоже на Point::MoveTo из базового для Circle класса Point. Действительно, возвращаемое значение, имя функции, сигнатура и даже само тело функции одинаковы. Мы знаем, что если С++ встречает два вызова функций, имеющих одно и то же имя, но разные сигнатуры, то компилятор разрешает эту потенциальную неоднозначность, используя перегрузку функций. Однако на первый взгляд, две рассматриваемых функции MoveTo не имеют видимых для компьютера различий. Поэтому неясно как он сумеет правильно выбрать требуемую функцию. Как было показано выше, в случае обычных функций компилятор определяет искомую функцию по типу объектов, указанных в вызове.
Итак, почему бы не позволить Circle наследовать MoveTo класса Point, аналогично тому, как СIrcle наследует функции GetX, GetY из Point через Location? Безусловно, причина, по которой этого нельзя делать, состоит в том, что Hide() и Show(), вызываемые в Circle::MoveTo, совсем не то же самое, что Hide и Show из Point::MoveTo. Одинаковые у них только сигнатуры. Поэтому наследование MoveTo из Point приведет к неправильному результату при попытке нарисовать окружность на экране. Это потому, что версии этих двух функций из Point будут связаны на этапе компиляции (раннее связывание) с функцией MoveTo из Point(и, следовательно, с Circle). Выход состоит в объявлении Hide() и Show() виртуальными функциями. В этом случае связывание будет задержано на время, достаточное для того, чтобы вызвать нужные версии функций Hide() и Show(), когда MoveTo используется для перемещения на экране точки, окружности или еще чего-нибудь.
Еще раз отметим, что если требуется предварительно откомпилировать определения классов и функций из Location, Point и Circle с целью помещения в отдельную библиотеку (при этом можно удержать в секрете от пользователей тонкости реализации), заранее невозможно предсказать какие объекты будут стараться перемещаться посредством MoveTo. Виртуальные функции не только предоставляют такую техническую возможность, но и обеспечивают важное концептуальное усиление, лежащее в основе ООП. Оно заключается в предоставлении возможности сконцентрироваться на разработке многократно используемых классов и их функций, не слишком беспокоясь из-за потенциальных конфликтов имен.
Несмотря на то, что возможность расширения библиотечных приложений присуща большинству языков программирования, наличие механизмов виртуальных функций и множественного наследования делает такое расширение более естественным. Наследуя все, что присуще базовым классам, одновременно можно включать дополнительные средства, чтобы заставить новые объекты работать надлежащим образом. Определяемые пользователем классы и версии виртуальных функций становятся действительными расширениями заданной иерархии возможностей.
Поскольку все эти средства были изначально заложены в язык С++ при его проектировании, а не стали его последующими противоестественными наращиваниями, их использование почти не сказывается на производительности.
Определение виртуальных функций.
Синтаксис весьма прост: добавляется модификатор virtual в первое объявление метода:
virtual void Show();
virtual void Hide();
Только член-функции могут объявляться виртуальными.
Если функция объявлена виртуальной, то она не должна объявляться повторно в любом производном классе с той же сигнатурой аргументов, но с другим типом возвращаемого значения. Если Show повторно объявляется с той же сигнатурой и типом возвращаемого значения, то новая функция Show становится виртуальной автоматически вне зависимости от того, используется модификатор virtual или нет. Говорят, что эта новая виртуальная Show перекрывает Show из базового класса.
Разрешается объявлять Show с другой сигнатурой формальных параметров (вне зависимости от типа возвращаемого значения), но в этом случае виртуальный механизм не используется. Начинающим следует избегать неоправданных перегрузок: имеется целый ряд ситуаций, в которых не виртуальная функция может закрыть виртуальную функцию, объявленную в базовом классе.
Какую функцию Show использовать, зависит только от класса объекта, для которого вызывается Show, даже если такой вызов инициируется обращением с помощью указателя (или ссылки) на базовый класс.
Например,
Circle ACircle(24,30,45);
Point* Apoint_pointer=&ACircle; // указатель на Circle
присваивается указателю на базовый класс, Point
APoint_pointer->Show(); // вызывает Circle::Show
Полный графический пример.
Создадим модуль, содержащий несколько видов классов и обобщающий средства перемещения их по экрану. Основная цель разработки этого модуля – дать пользователям возможность расширять классы, определенные в модуле, в то же время сохранив все его характеристики. В этом плане интересно разработать средства для перемещения произвольной фигуры по экрану в ответ на информацию, вводимую с клавиатуры.
В качестве первого шага создания такого модуля (назовем его FIGURE) рассмотрим функцию, принимающую объект как аргумент, а затем перемещающую его по экрану:
void Drad(Point & AnyFigure, int Dragby)
{ int DelX, DelY;
int FigX, FigY;
AnyFigure.Show(); //отображение перемещаемой
// фигуры
FigX=AnyFigure.GetX(); //Получение координат X,Y
FigY=AnyFigure.GetY(); //фигуры
// Цикл, перемещающий фигуру
// while (GetDelta(DelX,DelY))
{
Приращение к координатам
FigX=FigX+(DelX*Dragby);
FigY=FigY+(DelY*Dragby);
// Перемещение фигуры
AnyFigure.MoveTo(FigX,FigY);
}
}
Обратите внимание, что Any_Figure объявляется как имеющая тип Point &. Это означает буквально следующее: «ссылка на объект типа Point».
Drag вызывает не показанную здесь временную функцию GetDelta, которая получает от пользователя данные о некотором изменении координат X и Y. Они могут поступать с клавиатуры или от мыши. Для простоты положим, что они поступают от клавиш управления курсором клавиатуры.
Важной особенностью Drag является то, что любой объект типа Point или любой производный от Point могут передаваться по ссылке в аргументе AnyFigure. Объекты типа Point или Circle или любого другого определенного впоследствии как производного от Point или Circle типа, могут без затруднений передаваться в AnyFigure.
Добавление новой функции к существующей иерархии классов тривиально. В какое место поместить функцию? Это зависит от выполняемых ею действий и их универсальности. Так, например, перемещение фигуры включает изменение ее координат, как реакцию на ввод информации с клавиатуры. С точки зрения наследования эта функция должна быть расположена рядом с MoveTo- любой объект, которому соответствует MoveTo, должен также наследовать и Drag.
Поэтому Drag должна быть функцией Point и как следствие все производные от Point типы могут использовать ее.
Выяснив место Drag в иерархии, рассмотрим внимательнее его определение. Как метод базового класса Point, он не требует явной ссылки на аргумент AnyFigure типа Point &. Можно переписать Drag таким образом, что все вызываемые им функции, такие как GetY, Show, MoveTo и Hide будут правильно ссылаться на перемещаемый объект в зависимости от его типа. Как было показано ранее, функции Show и Hide, которым требуется тот или иной код в зависимости от формы объекта, могут быть сделаны виртуальными. Впоследствии они могут быть переопределены для любых новых классов без нарушения модуля FIGURE . При этом учитывается и MoveTo, поскольку последний будет вызывать нужные версии Show и Hide (напоминаем, что именно в этом состояло изначальное побуждение сделать Show и Hide виртуальными). GetX, GetY не представляют проблемы: как обычные функции, наследуемые из Point через Location, они просто возвращают элементы данных X, Y вызываемого объекта любого производного класса существующего или могущего появиться в будущем. Напоминаем, однако, что поскольку X и Y защищены в Location (protected), то следует использовать атрибут наследования public.
Следующий вопрос возникает при разработке – надо ли делать Drag виртуальной. Если предполагается, что выполняемые функцией действия могут измениться где-то на следующих уровнях иерархии, то такую функцию следует объявить виртуальной. При этом не существует универсального правила, но возможны некоторые рекомендации. Ниже мы рассмотрим некоторые из них.
Рассмотрим потенциально новый класс, ориентированный на работу с приложениями, в которых требуется специальное перемещение фигуры, масштабирование и т.п. Поэтому в новом определении класса Point в рамках figures.h функция Drag объявляется виртуальной.
class Point: public Location {
protected:
Boolean Visiable;
public:
Point (int initX, int initY);
virtual void Show();
virtual void Hide();
Boolean isVisiable() {return Visible;}
void MoveTo (int NewX, int NewY);
virtual void Drag(int Dragby);
};
Расмотрим теперь файл figures.h, содержащий объявления классов модуля FIGURE. Это единственная часть пакета, которая должна поставляться в исходном виде.
// figures.h содержит три класса:
// класс location определяет месторасположение на экране
// посредством координат X и Y
// класс Pount показывает является точка на экране видимой
// или нет
// класс Circle описывает радиус с центром в указанной
точке
Чтобы использовать этот модуль, нужно поместить
#include<figures.h> в головной файл и скомпилировать исходный файл FIGURE.CPP вместе с головным файлом.
enum Boolean {false, true};
class Location {
protected:
int X;
int Y;
public:
Location (int initX, int initY){X=initX;Y=initY}
int GetX() {return X;}
int GetY() {return Y;}
};
class Point : public Location{
protected:
Boolean Visible;
public:
Point (int initX, int initY);
virtual void Show();
virtual void Hide();
virtual void Drag(int Dragby); новая виртуальная ф-ция
перемещения
Boolean isVisible(){ return Visible; }
void MoveTo (imt NewX, int NewY);
};
class Circle : public Point
{
protected:
int Rad;
public:
Circle (int InitX, int InitY, int InitRad);
void Show();
void Hide();
void Expand ( iny Expandy);
void Contract (int ContractBy);
};
Добавим еще прототип функции общего назначения, не являющейся член-функцией и определенной в FIGURES.CPP
Boolean GetDelta( int & DeltaX, int & DeltaY);
Теперь проанализируем текст программы из файла FIGURES.CPP, в которой содержатся определения член-функций. В коммерческих продуктах эта часть пакета поставляется в объектном или в библиотечном виде. Заметим, что конструктор Circle был определен вне класса, так как инициирует базовые конструкторы.
Любители могут поэкспериментировать, сделав, этот конструктор встроенной функцией.
FIGURES.CPP- файл, содержащий определения для класса Point (объявленного в figures.h). Член-функции класса Location присутствуют в figures.h как встроенные функции.
#include » figures.h»
#include <grafics.h>
#include <conio.h>
функции класса Point:
конструктор
Point::Point(int InitX, int InitY) : Location(InitX, InitY)
{ Visible= false; – делает невидимым по умолчанию}
void Point ::Show()
{ Visible= true;
putpixel (X,Y, getcolor()); -использует цвет по умолчанию
}
void Point ::Hide()
{ Visible= false;
putpixel (X,Y, getcolor()); -для стирания использует
цвет фона
}
void Point ::MoveTo( int NewX, int NewY)
{ Hide(); делает текущую точку невидимой
X= NewX; Y=NewY;
Show(); отображает точку на новом месте
}
функция общего назначения для получения кодов перемещения
курсора (не член-функция)
Boolean GetDelta( int & DeltaX, int & DeltaY)
{ char KeyChar;
Boolean Quit;
DeltaX=0; DeltaY=0;
do{
KeyChar=getch(); – чтение кода с клавиатуры
if (KeyChar==13) return(false); – возврат каретки
if (KeyChar==0) – расширенный код
{ Quit = true; полагаем, что он используется
KeyChar=getch(); получаем второй байт кода
switch (KeyChar) {
case 72 : DeltaY==-1; break; – стрелка вниз
case 80 : DeltaY==1; break; – стрелка вверх
case 75 : DeltaX==-1; break; – стрелка влево
case 72 : DeltaX==1; break; – стрелка вправо
default:: Quit = false;
};
};
} while(!=Quit);
return(true);
}
void Point:: Drug(int DragBy)
{ int DeltaX, DeltaY;
int FigureX, FigureY;
Show();
FigureX=GetX(); получаем начальное положение
FigureX=GetX(); фигуры
Цикл перемещения по экрану
while (GetDelta (DeltaX, DeltaY))
{ FigureX+=(DeltaX*DragBy);
FigureY+=(DeltaY*DragBy);
MoveTo(FigureX, FigureY);
};
}
Функции класса Circle
конструктор
Circle::Circle(int InitX, int InitY, int InitRad):
Point(InitX,InitY)
{ Rad=InitRad;}
void Circle::Show(void)
{Visible=true; circle(X,Y,Rad);}- ф-ция рисования
окружности
void Circle::Hide(void)
{ unsigned int TempColor: сохранение текущего цвета
Tempcolor=getcolor(); установка текущего цвета
setcolor(getbkolor()); установка текущего цвета цветом
фона
Visible=false;
circle(X,Y,Rad); для стирания окружности рисуем
ее цветом фона
setcolor(TempColor()); Восстановление текущего цвета
}
void Circle:: Expand(int ExpandBy)
{ Hide(); стирание старой окружности
Rad+=ExpandBy; увеличение радиуса
if (Rad<0) Rad=0;
Show(); рисование новой окружности
}
void Circle::Contract(int ContractBy)
{ Expand(-ContractBy);}
Теперь все готово для проверки на прочность нашего модуля, содержащего исходные тексты наших член-функций. С этой целью введем новый класс Arc, производный от Circle с атрибутом public по определению. При этом стоит обратить внимание на то, что функции Drag предстоит перемещать фигуру, которую она никогда не видела.
#include «figures.h»
#include <graphics.h»
#include <conio.h>
class Arc : public Circle {
int StartAngle;
int EndAngle;
public:
Arc( int InitX, int InitY, int InitRad, int
InitStartAngle, int InitEndAngle) : Circle(
InitX, InitY, InitRad)
{StartAngle= InitStartAngle;EndAngle=InitEndAngle;}
void Show();
void Hide(); } – эти ф-ции являются виртуальными в
Point
Ф-ции Arc
void Arc::Show()
{ Visible=true; arc(X,Y, StartAngle, EndAngle,Rad);}void
void Arc::Hide()
{ unsigned int TempColor: сохранение текущего цвета
Tempcolor=getcolor(); установка текущего цвета
setcolor(getbkolor()); установка текущего цвета цветом
фона
Visible=false;
arc(X,Y, StartAngle, EndAngle,Rad);
для стирания дуги окружности
рисуем ее цветом фона
main() {
инициализация графики
Circle ACircle(151,82,50);
Arc AnArc(151,82,25,0,190);
AnArc.Drag(5);
AnArc.Hide();
ACircle.Drag(10);
closegraph();
Сначала перемещаем дугу, используя клавиши перемещения курсора (5 пикселов на символ), когда надоест, нажимаем Enter. Затем переместим окружность (10) пикселов – на нажатие клавиши стрелки. Для окончательного выхода из программы достаточно нажать Enter.
Обычные функции выполняются быстрее, чем виртуальные, поэтому они предпочтительнее, когда важна производительность. Подводя итог, отметим, что если объявляется некий класс Base, в рамках которого объявляется некая функция AA, то существуют следующее правило об описании этой функции как виртуальной:
Если существует вероятность того, что в рамках некоторого производного от Base класса будет использоваться функция, перекрывающая AA, которой нужен доступ к Base, то AA должна быть виртуальной. Функция АА должна быть обычной, если ясно, что в производных классах AA будет выполнять те же действия, что и в Base (даже, если это вызывает инициализацию других виртуальных функций) или же производные классы не будут использовать AA.