Нижегородский государственный университет им. Н.И.Лобачевского
Опубликован: 25.11.2008 | Доступ: свободный | Студентов: 9593 / 1296 | Оценка: 4.06 / 3.66 | Длительность: 21:16:00
Лекция 16:

Классы как средство создания больших программных комплексов

< Лекция 15 || Лекция 16: 123 || Лекция 17 >

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, которая теперь обращается не к виртуальным, а реальным методам класса pointhide и 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();
}
< Лекция 15 || Лекция 16: 123 || Лекция 17 >
Alexey Ku
Alexey Ku

Попробуйте часть кода до слова main заменить на 

#include "stdafx.h" //1

#include <iostream> //2
#include <conio.h>

using namespace std; //3

Александр Талеев
Александр Талеев

#include <iostream.h>
#include <conio.h>
int main(void)
{
int a,b,max;
cout << "a=5";
cin >> a;
cout <<"b=3";
cin >> b;
if(a>b) max=a;
else max=b;
cout <<" max="<<max;
getch();
return 0;
}

при запуске в visual express выдает ошибки 

Ошибка    1    error C1083: Не удается открыть файл включение: iostream.h: No such file or directory    c:\users\саня\documents\visual studio 2012\projects\проект3\проект3\исходный код.cpp    1    1    Проект3

    2    IntelliSense: не удается открыть источник файл "iostream.h"    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    1    1    Проект3

    3    IntelliSense: идентификатор "cout" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    6    1    Проект3

    4    IntelliSense: идентификатор "cin" не определен    c:\Users\Саня\Documents\Visual Studio 2012\Projects\Проект3\Проект3\Исходный код.cpp    7    1    Проект3

при создании файла я выбрал пустой проект. Может нужно было выбрать консольное приложение?