|
Попробуйте часть кода до слова main заменить на #include "stdafx.h" //1 #include <iostream> //2 using namespace std; //3 |
Классы как средство создания больших программных комплексов
15.2. Множественное наследование и виртуальные классы
О множественном наследовании говорят в тех случаях, когда в создании производного класса участвуют два или более родителей:
class B1 {//первый базовый класс
int x;
public:
B1(int n):x(n) {cout<<"Init_B1"<<endl;} //конструктор B1
int get_x(){return x;}
~B1() {cout<<"Destr_B1"<<endl;} //деструктор B1
};
class B2 {//второй базовый класс
int y;
public:
B2(int n):y(n) {cout<<"Init_B2"<<endl;} // конструктор B2
int get_y(){return y;}
~B2() {cout<<"Destr_B2"<<endl;} //деструктор B2
};
class D: public B1, public B2 {
int z;
public:
D(int a,int b,int c):B1(a),B2(b),z(c)
{cout<<"Init_D"<<endl;} //конструктор D
void show() {cout<<"x="<<get_x()<<" y="<<get_y()<<" z="<<z<<endl;}
~D() {cout<<"Destr_D"<<endl;} //деструктор D
};
#include <iostream.h>
void main()
{ D qq(1,2,3);
qq.show();
}
//=== Результат работы ===
Init_B1
Init_B2
Init_D
x=1 y=2 z=3
Destr_D
Destr_B2
Destr_B1
15.3.
Последовательность обращений к конструкторам родительских классов определяется очередностью их вызовов в списке инициализации. Если таковой отсутствует, то определяющим является порядок перечисления родителей в объявлении производного класса.
При множественном наследовании может возникнуть некоторая неопределенность, связанная с тем, что родительские данные могут попытаться попасть в производный класс несколькими путями. Например, классы A и B являются родителями класса C. Если в формировании класса D участвуют классы A и C, то данные-потомки класса A попадают в класс D и прямым путем, и в составе наследства класса C. И тогда перед компилятором возникает неразрешимая проблема – с какой веточкой унаследованных данных надо работать и методы какого класса надо вызывать. Для разрешения такой двойственности класс A должен быть объявлен виртуальным:
class B: virtual public A {
...//описание класса B
};
class C: virtual public A, public B {
//описание класса C
};При наследовании от виртуального класса производный класс вместо родительских данных получает ссылки на эти данные, что позволяет предотвратить дублирование наследства.
15.3. Объектно-ориентированный подход к созданию графической системы
Создание достаточно универсальной графической системы – это серьезный проект, требующий разработки большого количества разнообразных процедур. В их состав помимо средств объявления графических примитивов (точки, отрезки прямых, дуги окружностей, прямоугольники, пояснительные подписи и т.п.) и манипуляций с объектами (отображение на экране, стирание, перекраска, перемещение) должны входить различные вспомогательные утилиты. Например, такие как запоминание и восстановление фрагментов изображения, процедуры анимации, аппроксимации и сглаживания кривых, заливки и штриховки замкнутых областей, пересечения полигонов и многое другое.
Поэтому мы ограничимся лишь демонстрацией простейшей графической системы, имеющей в своем распоряжении минимальное число графических объектов – точки, окружности и залитые окружности. Эти объекты можно будет создавать в оперативной памяти, отображать на экране, делать невидимыми и перемещать по экрану в заданное место. Более того, для манипуляций с этими объектами мы воспользуемся существующей в среде BC 3.1 библиотекой процедур BGI (Borland Graphics Interface), обеспечивающих перевод экрана в простейший графический режим (режим VGA с разрешением 640x480) и отображение на нем графических примитивов. Однако детали работы с этой библиотекой мы постараемся скрыть от пользователя. Основная цель нашей демонстрации – показать главные аспекты объектно-ориентированного подхода на достаточно наглядном примере.
Описания наших новых классов, методов и вспомогательных утилит мы разместим в файле с именем gs.h (от Graphics System). По аналогии с работой с файлами нам понадобятся процедуры открытия (инициализации) графической системы и ее закрытия. Для этого мы включим в файл gs.h следующий фрагмент:
#include <graphics.h>
int gs;
void open_gs()
{ int gd=0,gm;
initgraph(&gd,&gm,"");
gs=1;
}
void close_gs()
{ closegraph(); gs=0; }Заголовочный файл graphics.h содержит заголовки функций и описания констант библиотеки BGI. Переменная gs, может быть, понадобится в будущем для индикации готовности графической системы к работе (при gs=1 система открыта для работы с графическими объектами, закрытие системы сопровождается засылкой нуля в переменную gs ). Для приведения библиотеки BGI в состояние готовности используется процедура initgraph и графический драйвер egavga.bgi, который мы из соображений удобства разместим в своем текущем каталоге. Восстановление текстового режима работы дисплея осуществляется процедурой closegraph из библиотеки BGI. Однако пользователь о деталях работы с процедурами BGI ничего знать не должен. Для "открытия" графической системы он должен обратиться к процедуре open_gs, а для закрытия – к процедуре close_gs (почти полная аналогия открытия и закрытия файлов).
Описание нашей графической системы мы начнем с абстрактного класса GO (от Graphics Object).
class GO {
protected:
int x,y,is_v,fc,bc;
public:
GO():x(0),y(0),is_v(0),bc(15),fc(0)
{ setcolor(fc);setbkcolor(bc); }
GO(int x1,int y1,int c=0):x(x1),y(y1),is_v(0),fc(c),bc(15)
{ setcolor(fc);setbkcolor(bc); }
virtual void hide()=0;
virtual void show()=0;
void move(int x1,int y1);
};Защищенными данными в этом классе являются:
- x,y – целочисленные координаты точки привязки графического объекта в системе координат экрана (для объекта "точка" это координаты точки, для окружности – координаты центра);
- is_v – индикатор видимости (видимому на экране объекту соответствует is_v=1 );
- fc – цвет рисования (целое число из диапазона [0,15]);
- bc – цвет фона (целое число из диапазона [0,15]).
Конструктор по умолчанию считает, что точкой привязки графического объекта является начало координат (верхний левый угол экрана). С помощью процедуры setcolor устанавливается черный цвет рисования ( fc=0 ), а с помощью процедуры setbkcolor – белый цвет фона ( bc=15 ).
В классе GO объявлены два чисто виртуальных метода – hide (стереть изображение объекта) и show (отобразить объект). Метод move осуществляет перемещение объекта в новую точку привязки и не является виртуальным. Поэтому мы его определим за пределами описания класса:
void GO::move(int x1,int y1)
{ hide(); //стереть прежнее изображение объекта
x=x1; y=y1; //изменить координаты точки привязки
show(); //отобразить объект в новом месте
}Теперь определим производный класс point, с помощью которого вводятся объекты типа "точка" и манипуляции с объектами этого типа. Новый класс наследует от класса GO все данные (повторять их в классе point не надо). Конструкторы класса point явно вызывают конструкторы родителя, передавая им в случае необходимости недостающие параметры.
class point: public GO {
public:
point():GO() {}
point(int x1,int y1,int c=0):GO(x1,y1,c) {}
void hide();
void show();
};В классе point переопределяются наследуемые виртуальные методы. Для стирания изображения видимой точки используется процедура putpixel, которая "рисует" точку цветом фона. Для отображения невидимой точки используется та же процедура с заданным значением цвета.
void point::hide() //стирание точки
{ if(is_v) { putpixel(x,y,bc); is_v=0; } }
void point::show() //отображение точки
{ if(!is_v) { putpixel(x,y,fc); is_v=1; } }Для перемещения точки сохраняется родительская процедура move, которая теперь обращается не к виртуальным, а реальным методам класса point – hide и show.
Добавим класс circ, производный от класса GO и предназначенный для работы с объектами типа "окружность". В дополнение к данным, унаследованным от родителя, здесь понадобится еще и радиус окружности (переменная r )
class circ: public GO {
int r;
public:
circ():GO(),r(1){ }
circ(int x1,int y1,int r1,int c=0): GO(x1,y1,c),r(r1) { }
void hide();
void show();
};Унаследованные виртуальные методы hide и show здесь также придется переопределить. Для стирания видимой окружности используем процедуру построения объекта, задав в качестве цвета рисования цвет фона.
void circ::hide() //стирание окружности
{ if(is_v==0) return;
int fc1=getcolor(); //запоминание цвета рисования
setcolor(bc); //замена цвета рисования на цвет фона
circle(x,y,r); //построение окружности
setcolor(fc1); //восстановление цвета рисования
is_v=0;
}
void circ::show() //отображение окружности
{ if(is_v) return;
int fc1=getcolor(); //запоминание цвета рисования
setcolor(fc); //замена на цвет объекта
circle(x,y,r); //построение окружности
setcolor(fc1); //восстановление цвета рисования
is_v=1;
}Класс circf для работы с залитыми окружностями тоже образуем из класса GO.
class circf: public GO {
int r;
public:
circf():GO(),r(1){}
circf(int x1,int y1,int r1,int c=0):r(r1),GO(x1,y1,c) {}
void show();
void hide();
};Для реализации метода show воспользуемся процедурой построения залитого эллипса – fillellipse. Но предварительно потребуется задать шаблон заливки, соответствующий сплошному заполнению замкнутой области (графическая константа SOLID_FILL=1 ), и цвет заливки, равный цвету объекта (значение переменной fc ). Обе эти установки выполняются библиотечной процедурой setfillstyle.
void circf::show()
{ if(is_v) return;
setfillstyle(1,fc); //установка стиля и цвета заливки
fillellipse(x,y,r,r); //построение залитой окружности
is_v=1;
}Метод hide оказался не совсем тривиальным, т.к. после построения эллипса, залитого цветом фона, сохраняется цветная граница. Поэтому приходится выполнить еще одно построение – нарисовать контуры окружности цветом фона.
void circf::hide()
{ if(!is_v) return;
setfillstyle(1,bc); //установка стиля и цвета заливки
fillellipse(x,y,r,r); //стирание залитой окружности
setcolor(bc); //замена цвета рисования на цвет фона
circle(x,y,r); //стирание границы окружности
setcolor(fc); //восстановление цвета рисования
is_v=0;
}А теперь настало время апробировать нашу графическую систему. Если забыть о содержимом файла gs.h и о времени, затраченном на его создание, то работа с допустимым набором графических объектов выглядит абсолютно прозрачно. После каждой графической манипуляции организована пауза в работе программы до нажатия любой клавиши. Это дает возможность рассмотреть на экране результат очередной операции.
#include "gs.h"
#include <conio.h>
void main()
{ open_gs(); //открытие графической системы
//Объявление графических объектов
point P1(21,10,2); //зеленая точка (21,10)
circ C1(21,50,20,4); //красная окружность радиуса 20
circf CF1(21,100,20,12); //залитая окружность
//Отображение графических объектов
P1.show(); getch(); //показ точки
C1.show(); getch(); //показ окружности
CF1.show(); getch(); //показ залитой окружности
//Перемещение графических объектов
P1.move(121,10); getch(); //сдвиг точки
C1.move(121,50); getch(); //сдвиг окружности
CF1.move(121,100); getch(); //сдвиг залитой окружности
//Стирание графических объектов
P1.hide(); getch(); //стирание точки
C1.hide(); getch(); //стирание окружности
CF1.hide(); getch(); //стирание залитой окружности
// Перемещение графических объектов
P1.move(221,10); getch(); //сдвиг точки
C1.move(221,50); getch(); //сдвиг окружности
CF1.move(221,100); getch(); //сдвиг залитой окружности
close_gs(); //закрытие графической системы
}Можно создать массив указателей на объекты класса GO и заполнить его адресами графических объектов разного типа. Применение к этим указателям методов с одними и теми же названиями, приводит к вызовам методов тех классов, чей тип совпадает с типом адресуемого объекта. Это и есть демонстрация одного из важнейших принципов объектно- ориентированного подхода – полиморфизма:
#include "gs.h"
#include <conio.h>
void main()
{ open_gs();
point P1(21,10,2);
circ C1(21,50,20,4);
circf CF1(21,100,20,12);
GO *m[3]={&P1,&C1,&CF1}; //массив указателей
for(int i=0;i<3;i++) m[i]->show(); //отображение объектов
getch();
m[0]->move(121,10); getch(); //сдвиг точки
m[1]->move(121,50); getch(); //сдвиг окружности
m[2]->move(121,100); getch(); //сдвиг залитой окружности
close_gs();
}