Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Кетков.doc
Скачиваний:
17
Добавлен:
27.09.2019
Размер:
2.22 Mб
Скачать

16.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

Последовательность обращений к конструкторам родительских классов определяется очередностью их вызовов в списке инициализации. Если таковой отсутствует, то определяющим является порядок перечисления родителей в объявлении производного класса.

При множественном наследовании может возникнуть некоторая неопределенность, связанная с тем, что родительские данные могут попытаться попасть в производный класс несколькими путями. Например, классы A и B являются родителями класса C. Если в формировании класса D участвуют классы A и C, то данные-потомки класса A попадают в класс D и прямым путем, и в составе наследства класса C. И тогда перед компилятором возникает неразрешимая проблема – с какой веточкой унаследованных данных надо работать и методы какого класса надо вызывать. Для разрешения такой двойственности класс A должен быть объявлен виртуальным:

class B: virtual public A {

...//описание класса B

};

class C: virtual public A, public B {

//описание класса C

};

При наследовании от виртуального класса производный класс вместо родительских данных получает ссылки на эти данные, что позволяет предотвратить дублирование наследства.

16.3. Объектно-ориентированный подход к созданию графической системы

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

Поэтому мы ограничимся лишь демонстрацией простейшей графической системы, имеющей в своем распоряжении минимальное число графических объектов – точки, окружности и залитые окружности. Эти объекты можно будет создавать в оперативной памяти, отображать на экране, делать невидимыми и перемещать по экрану в заданное место. Более того, для манипуляций с этими объектами мы воспользуемся существующей в среде BC 3.1 библиотекой процедур BGI (Borland Graphics Interface), обеспечивающих перевод экрана в простейший графический режим (режим VGA с разрешением 640×480) и отображение на нем графических примитивов. Однако детали работы с этой библиотекой мы постараемся скрыть от пользователя. Основная цель нашей демонстрации – показать главные аспекты объектно-ориентированного подхода на достаточно наглядном примере.

Описания наших новых классов, методов и вспомогательных утилит мы разместим в файле с именем 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();

}