Попробуйте часть кода до слова 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_B115.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(); }