Введение в языки программирования C и C++

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

Разбить на страницы
Показывать лекцию целиком

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

15.1. Базовый и производный классы

Когда говорят о классе D, порожденном из класса B, то принято называть родительский класс базовым, а вновь созданный класс – производным. Механизм наследования ( inheritance ) предусматривает две возможности. В первом случае, который называют простым наследованием, родительский класс один. Во втором случае родителей два или больше, и соответствующий процесс именуют термином множественное наследование. В первую очередь мы познакомимся с механизмом простого наследования.

15.1.1.Простое наследование

Итак, как формально выглядит процедура объявления производного класса D и что он получает в наследство от своего родителя – класса B?

class D: [virtual][public|private|protected] B 
         {тело производного класса};

Служебное слово virtual (виртуальный) используется для предотвращения коллизий в случае сложного множественного наследования (по этому поводу см. раздел 15.2). Кроме уровней доступа public ( общедоступный ) и private ( личный ) в классах, создаваемых на базе структур ( struct ) и настоящих классов ( class ), используется еще один уровень защиты – protected ( защищенный ). Защищенными данными класса могут пользоваться функции и методы самого класса, производных классов и дружественные функции. При создании производного класса D может быть упомянут один из этих уровней доступа, что повлияет на изменение уровня доступа к унаследованным данным и функциям. По этому поводу в стандарте C++ существует целая таблица:

Уровень доступа в B Уровень доступа при объявлении D Уровень доступа в D
D=struc D=class
public опущен public private
protected опущен public private
private опущен нет доступа нет доступа
public public public public
protected public protected protected
private public нет доступа нет доступа
public protected protected protected
protected protected protected protected
private protected нет доступа нет доступа
public private private private
protected private private private
private private нет доступа нет доступа

В современной практике программирования действует общепринятое правило – родителями и потомками должны быть только настоящие классы. Поэтому о существовании третьей колонки в табл. 15.1 можно сразу забыть.

Чаще всего производный класс конструируют по следующей схеме, которая носит название открытого наследования:

class D: public B {тело производного класса};

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

Однако приведенное выше утверждение не распространяется на конструкторы и деструкторы. Они не наследуются, но к ним можно обратиться с добавлением принадлежности классу B.

В приведенном ниже примере имеет место открытое наследование производного класса D от своего родителя. Поле данных x родительского класса для потомка закрыто, но методы setb и showb сохраняют в классе D уровень доступа public. Поэтому с объектом типа D к этим методам обращаться можно:

#include <iostream.h>
#include <conio.h>
class B {
  int b;
public:
  void setb(int n){b=n;}
  void showb(){cout<<"in B b="<<b<<endl;}
};
class D: public B {
  int d;
public:
  void setd(int n){d=n;}
  void showd(){cout<<"in D d="<<d<<endl;}
};
void main()
{ D qq;		//объявление объекта порожденного класса
  qq.setb(1); 	//доступ к члену базового класса qq.x
  qq.showb(); 	//доступ к члену базового класса
  qq.setd(2); 	//доступ к члену производного класса qq.y
  qq.showd(); 	//доступ к члену производного класса
  qq.showb();	//доступ к члену базового класса
  getch();
}
//=== Результат работы ===
in B b=1	//qq.x
in D d=2	//qq.y
in B b=1	//qq.x

Обратите внимание на то, что после обращения к методу setd значение поля qq.x не изменилось.

А теперь модифицируем уровень доступа при объявлении производного класса:

class D: private B {
  int d;
public:
  void setbd(int n,int m)
{ setb(n);	//для класса D функция стала private, но она доступна
  d=m; }
  void showbd()
{ showb();	// для класса D функция стала private, но она доступна
  cout<<"in D d="<<d<<endl;}
};
void main()
{ D qq;		//объявление объекта порожденного класса
  qq.setbd(1,2);
  qq.showbd();
  getch();
}

Результат работы программы прежний, но в доступе к методам класса B помог производный класс. В последнем примере можно заменить в объявлении класса D уровень доступа на protected – функции setb и showb получат в классе D статус protected, но они по-прежнему будут доступны, и результат работы программы будет прежним.

15.1.2. Вызов конструкторов и деструкторов при наследовании

Последовательность вызова конструкторов и деструкторов легче проследить на следующем примере. В базовом классе B содержится единственный закрытый член данных x, предусмотрены три конструктора (по умолчанию, инициализации и копирования), функция опроса значения закрытого поля и деструктор. Каждый из них выводит свое условное обозначение при вызове. В производном классе D, который наследует поле x в режиме private, содержится и собственное закрытое поле y. В его составе такие же три конструктора, функция опроса значения закрытого поля и деструктор.

Головная программа сначала создает четыре объекта w1, w2, w3 и w4 типа B, а затем четыре объекта q1, q2, q3 и q4 типа D. После создания каждого объекта фиксируется содержимое соответствующих полей и цепочка вызываемых конструкторов. Перед окончанием программы фиксируется цепочка вызовов деструкторов.

#include <iostream.h>
#include <conio.h>
class B {
  int x;
public:
  B(){x=0; cout<<"Def_B "<<endl;}
  B(int n){x=n; cout<<"Init_B "<<endl;}
  B(const B y){x=y.x; cout<<"Copy_B "<<endl;}
  int get_x(){return x;}
  ~B(){cout<<"Destr_B"<<endl;}
};
class D : public B {
  int y;
public:
  D(){y=0; cout<<"Def_D "<<endl;}
  D(int n){y=n; cout<<"Init_D "<<endl;}
  D(const D z){y=z.y; cout<<"Copy_D "<<endl;}
  int get_y(){return y;}
  ~D(){cout<<"Destr_D"<<endl;}
};
void main()
{ B w1;
  cout<<"w1.x="<<w1.get_x()<<endl;
  B w2(2);
  cout<<"w2.x="<<w2.get_x()<<endl;
  B w3(w2);
  cout<<"w3.x="<<w3.get_x()<<endl;
  B w4=w1;
  cout<<"w4.x="<<w4.get_x()<<endl;
  D q1;
  cout<<"q1.x="<<q1.get_x()<<' '<<"q1.y="<<q1.get_y()<<endl;
  D q2(2);
  cout<<"q2.x="<<q2.get_x()<<' '<<"q2.y="<<q2.get_y()<<endl;
  D q3(q2);
  cout<<"q3.x="<<q3.get_x()<<' '<<"q3.y="<<q3.get_y()<<endl;
  D q4=q1;
  cout<<"q4.x="<<q4.get_x()<<' '<<"q4.y="<<q4.get_y()<<endl;
}
//=== Результаты работы ===
Def_B 		//конструктор B по умолчанию для создания w1.x
w1.x=0		//значение созданного объекта
Init_B 		//конструктор B инициализации для создания w2.x
w2.x=2		//значение созданного объекта
Copy_B 		//конструктор B копирования для создания w3.x
w3.x=2		//значение созданного объекта
Copy_B		//конструктор B копирования для создания w4.x
w4.x=0		//значение созданного объекта
Def_B 		//неявный вызов конструктора B для создания q1.x
Def_D 		//конструктор D по умолчанию для создания q1.y
q1.x=0 q1.y=0	//значения созданных объектов
Def_B 		//неявный вызов конструктора B для создания q2.x
Init_D 		//конструктор D инициализации для создания q2.y
q2.x=0 q2.y=2	//значения созданных объектов
Def_B 		//неявный вызов конструктора B для создания q3.x
Copy_D 		//конструктор D копирования для создания w3.y
q3.x=0 q3.y=2	//значения созданных объектов
Def_B 		//неявный вызов конструктора B для создания q4.x
Copy_D 		//конструктор D копирования для создания w4.y
q4.x=0 q4.y=0	//значения созданных объектов
Destr_D		//деструктор D для уничтожения w4.y
Destr_B		//деструктор B для уничтожения w4.x
Destr_D		//деструктор D для уничтожения w3.y
Destr_B		//деструктор B для уничтожения w3.x
Destr_D		//деструктор D для уничтожения w2.y
Destr_B		//деструктор B для уничтожения w2.x
Destr_D		//деструктор D для уничтожения w1.y
Destr_B		//деструктор B для уничтожения w1.x
Destr_B		//деструктор B для уничтожения q4.x
Destr_B		//деструктор B для уничтожения q3.x
Destr_B		//деструктор B для уничтожения q2.x
Destr_B		//деструктор B для уничтожения q1.x

Обратите внимание на то, что в этом примере создание объектов производного класса начинается с автоматического вызова конструктора базового класса по умолчанию. Кроме того, объекты уничтожаются в порядке, обратном последовательности их создания – самый первый объект разрушается последним.

Однако возможна ситуация, когда ни программист, ни система не включили в базовый класс конструктор по умолчанию. Это происходит в тех случаях, когда программист написал только конструкторы с параметрами. В такой ситуации конструкторы производного класса должны сами позаботиться об инициализации объектов родительского класса. Сделать это можно разными способами – явно вызвать конструктор базового класса либо в своем списке инициализации, либо в теле конструктора. Для защищенных ( protected ) полей базового класса можно воспользоваться указателем this. В приводимом ниже примере демонстрируются эти возможности. В качестве базового класса выступает класс Point2D, моделирующий точку на плоскости:

class Point2D {
  int x,y;	//закрытые данные класса Point2D
public:
  Point2D(int xx,int yy):x(xx),y(yy){} //конструктор инициализации
  Point2D(const Point2D P):x(P.x),y(P.y){} //конструктор копирования
  int get_x(){return x;}
  int get_y(){return y;}
};

Порожденный класс Point3D моделирует точку в трехмерном пространстве:

class Point3D: public Point2D {
  int z;    //новая координата в классе Point3D
public:
  Point3D(int xx,int yy,int zz):Point2D(xx,yy),z(zz){}
  int get_z(){return z;}    //новый метод в классе Point3D
};

А теперь протестируем оба класса на следующей программе:

#include <iostream.h>
#include <conio.h>
void main()
{ Point2D P2(1,2);
  Point3D P3(3,4,5);
  cout<<"P3.x="<<P3.get_x()<<" P3.y="<<P3.get_y()<<" 
      P3.z=" <<P3.get_z()<<endl;
  cout<<"P2.x="<<P2.get_x()<<" P2.y="<<P2.get_y()<<endl;
  P2=P3;
  cout<<"P2.x="<<P2.get_x()<<" P2.y="<<P2.get_y()<<endl;
  getch();
}
//=== Результат работы ===
P3.x=3 P3.y=4 P3.z=5
P2.x=1 P2.y=2
P2.x=3 P2.y=4

Производному классу по наследству достались приватные данные – координаты ( x,y ) родительского объекта и общедоступные методы доступа к этим координатам. Поэтому в головной программе мы можем пользоваться этими методами как по отношению к объектам типа Point2D, так и по отношению к объектам типа Point3D. Немного странным кажется оператор присваивания двухмерному объекту P2 значения трехмерного объекта P3. Но происходит вполне естественная операция – те поля, которые являются общими у этих двух объектов, переносятся, а "лишнее" поле P3.z отсекается. Обратная операция P3=P2 была бы ошибочной, т.к. компилятор не "знает", чем следует заполнить поле P3.z.

Если бы поля ( x,y ) в базовом классе были объявлены как защищенные ( protected ), то их инициализацию в конструкторе производного класса можно было бы выполнить и так:

Point3D(int xx,int yy,int zz):z(zz)
         { this->x=xx; this->y=yy; }

15.1.3. Динамическое создание и удаление объектов

Объявление объектов с использованием конструкторов создает данные, которые существуют до выхода из блока, в котором они появились. Однако иногда объекты могут потребоваться на более короткое время. Такие объекты можно создавать и уничтожать во время работы программы с помощью операторов new и delete:

class A {...};    //объявление класса
..............
A *ps=new A;      //объявление указателя и создание объекта типа A
A* *pa=new A[20]; //объявление указателя и создание массива объектов
...............
delete ps;        //удаление объекта по указателю ps
delete [] pa;     //удаление массива объектов по указателю pa

Фактически, выполнение оператора new эквивалентно вызову конструктора класса, а обращение к оператору delete на автомате означает вызов деструктора. Создание одиночных объектов может быть совмещено с инициализацией объекта, если в классе предусмотрен соответствующий конструктор:

A *ptr1=new A(5);//создание объекта и вызов конструктора инициализации

Массив создаваемых объектов проинициализировать таким же образом нельзя.

В ранних версиях C++ для создания и уничтожения динамических объектов использовали обращения к функциям malloc ( запрос памяти ) и free ( освобождение памяти ). Неудобство применения этих функций по сравнению с операторами new/delete заключается в том, что для запроса памяти нужно знать количество байт, занимаемых объектом в оперативной памяти. Конечно, это не так уж и сложно – существует функция sizeof, с помощью которой длину объекта можно определить. Второе неудобство заключается в том, что функция malloc выдает указатель типа void* и его еще надо преобразовать к типу указателя на объект класса.

Довольно распространенная ситуация, которая может оказаться потенциальным источником ошибок, возникает в процессе создания и удаления динамических объектов. Она заключается в том, что после уничтожения объекта, связанного, например, с указателем ps, этот указатель не чистится. Если после удаления объекта сделать попытку что-то прочитать или записать по этому указателю, то поведение программы предсказать трудно. Поэтому достаточно разумным правилом является засылка нуля в указатель разрушаемого объекта:

delete ps;
  ps=NULL; 	//или ps=0;

15.1.4. Виртуальные функции

Виртуальными называют функции базового класса, которые могут быть переопределены в производном классе. В базовом классе их заголовок начинается со служебного слова virtual. В производном классе такая функция продолжает оставаться виртуальной, даже если перед ее заголовком слово virtual опущено. Однако на практике рекомендуют и в производном классе перед заголовком переопределяемой виртуальной функции указывать термин virtual.

Заголовки виртуальных функций в базовом и производном классах должны быть обязательно идентичными. Поэтому переопределение распространяется только на тело функции. Это позволяет обращаться к виртуальным функциям, не указывая их принадлежность тому или иному классу. Выбором нужной функции управляет тип объекта, заданный явно или неявно – через указатель, который может быть объявлен как указатель родительского класса. Если в производном классе виртуальная функция не переопределяется, то к объектам порожденного класса применяется родительский виртуальный метод.

Рассмотрим пример, в котором базовый класс B содержит защищенное поле n и отображает его содержимое на экране. Производный класс D1 отображает квадрат доставшегося по наследству поля. Еще один класс D2, порожденный тем же родителем B, отображает куб своего наследства.

#include <iostream.h>
#include <conio.h>
class B {
public:
  B(int k):n(k){}	//конструктор инициализации
  virtual void show(){cout<<n<<endl;}	//виртуальная функция
protected:
  int n;
};
class D1: public B {
public:
  D1(int k):B(k){}	// конструктор инициализации
  virtual void show(){cout<<n*n<<endl;}
};
class D2: public B {
public:
  D2(int k):B(k){}	// конструктор инициализации
  virtual void show(){cout<<n*n*n<<endl;}
};
void main()
{ B bb(2),*ptr;
  D1 dd1(2);
  D2 dd2(2);
  ptr=bb;
  ptr->show();
  ptr=dd1;
  ptr->show();
  ptr=dd2;
  ptr->show();
  getch();
}
//=== Результат работы ===
2	//результат работы функции B::show
4	//результат работы функции D1::show
8	//результат работы функции D2::show

Обратите внимание на то, что в предыдущем примере адреса объектов производных классов присваиваются указателю, чей тип был связан с объектами базового класса. Такое присвоение можно делать без явного приведения типов. А вот обратное преобразование указателя базового класса в указатель производного класса сопровождается явным преобразованием типа:

B *bptr;
  D1 dd1(2);
  bptr=dd1;        //вниз по иерархии классов без преобразования
  D1 *dptr;
  dptr=(D1 *)bptr;  //вверх по иерархии с преобразованием типа

15.1.5. Виртуальные деструкторы

Когда существует иерархия производных классов, и мы создаем массив динамических указателей на объекты разных производных классов, то при уничтожении такого рода объектов могут возникнуть проблемы. Продемонстрируем это на примере иерархии геометрических фигур: Shape (фигура базового класса), Circle (окружность, производная от Shape ) и Rectangle (прямоугольник, производный от Shape ):

#include <iostream.h>
class Shape {
public:
  Shape();		//конструктор по умолчанию
  ~Shape();	//стандартный деструктор
  virtual void show() {cout <<"Shape"<<endl;
};
class Circle: public Shape {
  int xc,yc,r;	//координаты центра и радиус
public:
  Circle(int x,int y,int R):xc(x),yc(y),r(R) {}	//конструктор
  ~Circle();	//стандартный деструктор
  void show() {cout<<"x="<<xc<<" 
    y="<<yc<<" r="<<r<<endl;
};
class Rectangle: public Shape {
  int x1,y1,x2,y2;	//координаты противоположных вершин
public:
  Rectangle(int ix1,int iy1,int ix2,int iy2):
		 x1(ix1),y1(iy1),x2(ux2),y2(iy2) {}	//конструктор
  ~Rectangle();	//стандартный деструктор

Создаем массив указателей на объекты базового класса и присваиваем им адреса динамически создаваемых объектов:

Shape *ptr_s[2];
  ptr_s[0]=new Circle(20,20,10)
  ptr_s[1]=new Rectangle(20,40,50,50);

Поработали с временно созданными объектами, и пришла пора их удалить. Попытка сделать это следующим способом ни к чему хорошему не приведет:

for(int i=0; i<2; i++) delete ptr_s[i];

Причина заключается в том, что для удаления этих фигур будет вызван деструктор класса Shape (именно на объекты этого класса был объявлен массив указателей ptr_s ). А ресурсы, занятые окружностью и прямоугольником, при этом не будут освобождены. Выход из создавшегося положения довольно простой – надо объявить деструктор базового класса виртуальным ( virtual ~Shape(); ). Тогда автоматически виртуальными станут и деструкторы производных классов (хотя деструкторы и не наследуются). И все проблемы, связанные с утечкой памяти, будут решены.

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

15.1.6. Чистые виртуальные функции и абстрактные классы

Чистая виртуальная функция не совершает никаких действий, и ее описание выглядит следующим образом:

virtual тип name_f(тип1 a1,тип2 a2,...)=0;

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

Объявим абстрактным класс Shape (Геометрическая Фигура), в состав которого включим две чистые виртуальные функции – определение площади фигуры ( Get_Area ) и определение периметра фигуры ( Get_Perim ).

class Shape {
public:
  Shape(){}	//конструктор
  virtual double Get_Area()=0;
  virtual double Get_Perim()=0;
};
class Rectangle: public Shape {
  double w,h;	//ширина и высота
public:
  Rectangle(double w1,double h1):w(w1),h(h1) {}
  double Get_Area() {return w*h;}
  double Get_Perim() {return 2*w+2*h);}
};
class Circle: public Shape {
  double r;	//радиус
public:
  Circle(double r1):r(r1) {}
  double Get_Area() {return M_PI*r*r;}
  double Get_Perim() {return 2*M_PI*r;}
};

Если в производном классе хотя бы одна из чисто виртуальных функций не переопределяется, то производный класс продолжает оставаться абстрактным и попытка создать объект (экземпляр класса) будет пресечена компилятором.

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

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

При множественном наследовании может возникнуть некоторая неопределенность, связанная с тем, что родительские данные могут попытаться попасть в производный класс несколькими путями. Например, классы 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();
    }
    Страницы:

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

    15.1. Базовый и производный классы

    Когда говорят о классе D, порожденном из класса B, то принято называть родительский класс базовым, а вновь созданный класс – производным. Механизм наследования ( inheritance ) предусматривает две возможности. В первом случае, который называют простым наследованием, родительский класс один. Во втором случае родителей два или больше, и соответствующий процесс именуют термином множественное наследование. В первую очередь мы познакомимся с механизмом простого наследования.

    15.1.1.Простое наследование

    Итак, как формально выглядит процедура объявления производного класса D и что он получает в наследство от своего родителя – класса B?

    class D: [virtual][public|private|protected] B 
             {тело производного класса};

    Служебное слово virtual (виртуальный) используется для предотвращения коллизий в случае сложного множественного наследования (по этому поводу см. раздел 15.2). Кроме уровней доступа public ( общедоступный ) и private ( личный ) в классах, создаваемых на базе структур ( struct ) и настоящих классов ( class ), используется еще один уровень защиты – protected ( защищенный ). Защищенными данными класса могут пользоваться функции и методы самого класса, производных классов и дружественные функции. При создании производного класса D может быть упомянут один из этих уровней доступа, что повлияет на изменение уровня доступа к унаследованным данным и функциям. По этому поводу в стандарте C++ существует целая таблица:

    Уровень доступа в B Уровень доступа при объявлении D Уровень доступа в D
    D=struc D=class
    public опущен public private
    protected опущен public private
    private опущен нет доступа нет доступа
    public public public public
    protected public protected protected
    private public нет доступа нет доступа
    public protected protected protected
    protected protected protected protected
    private protected нет доступа нет доступа
    public private private private
    protected private private private
    private private нет доступа нет доступа

    В современной практике программирования действует общепринятое правило – родителями и потомками должны быть только настоящие классы. Поэтому о существовании третьей колонки в табл. 15.1 можно сразу забыть.

    Чаще всего производный класс конструируют по следующей схеме, которая носит название открытого наследования:

    class D: public B {тело производного класса};

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

    Однако приведенное выше утверждение не распространяется на конструкторы и деструкторы. Они не наследуются, но к ним можно обратиться с добавлением принадлежности классу B.

    В приведенном ниже примере имеет место открытое наследование производного класса D от своего родителя. Поле данных x родительского класса для потомка закрыто, но методы setb и showb сохраняют в классе D уровень доступа public. Поэтому с объектом типа D к этим методам обращаться можно:

    #include <iostream.h>
    #include <conio.h>
    class B {
      int b;
    public:
      void setb(int n){b=n;}
      void showb(){cout<<"in B b="<<b<<endl;}
    };
    class D: public B {
      int d;
    public:
      void setd(int n){d=n;}
      void showd(){cout<<"in D d="<<d<<endl;}
    };
    void main()
    { D qq;		//объявление объекта порожденного класса
      qq.setb(1); 	//доступ к члену базового класса qq.x
      qq.showb(); 	//доступ к члену базового класса
      qq.setd(2); 	//доступ к члену производного класса qq.y
      qq.showd(); 	//доступ к члену производного класса
      qq.showb();	//доступ к члену базового класса
      getch();
    }
    //=== Результат работы ===
    in B b=1	//qq.x
    in D d=2	//qq.y
    in B b=1	//qq.x

    Обратите внимание на то, что после обращения к методу setd значение поля qq.x не изменилось.

    А теперь модифицируем уровень доступа при объявлении производного класса:

    class D: private B {
      int d;
    public:
      void setbd(int n,int m)
    { setb(n);	//для класса D функция стала private, но она доступна
      d=m; }
      void showbd()
    { showb();	// для класса D функция стала private, но она доступна
      cout<<"in D d="<<d<<endl;}
    };
    void main()
    { D qq;		//объявление объекта порожденного класса
      qq.setbd(1,2);
      qq.showbd();
      getch();
    }

    Результат работы программы прежний, но в доступе к методам класса B помог производный класс. В последнем примере можно заменить в объявлении класса D уровень доступа на protected – функции setb и showb получат в классе D статус protected, но они по-прежнему будут доступны, и результат работы программы будет прежним.

    15.1.2. Вызов конструкторов и деструкторов при наследовании

    Последовательность вызова конструкторов и деструкторов легче проследить на следующем примере. В базовом классе B содержится единственный закрытый член данных x, предусмотрены три конструктора (по умолчанию, инициализации и копирования), функция опроса значения закрытого поля и деструктор. Каждый из них выводит свое условное обозначение при вызове. В производном классе D, который наследует поле x в режиме private, содержится и собственное закрытое поле y. В его составе такие же три конструктора, функция опроса значения закрытого поля и деструктор.

    Головная программа сначала создает четыре объекта w1, w2, w3 и w4 типа B, а затем четыре объекта q1, q2, q3 и q4 типа D. После создания каждого объекта фиксируется содержимое соответствующих полей и цепочка вызываемых конструкторов. Перед окончанием программы фиксируется цепочка вызовов деструкторов.

    #include <iostream.h>
    #include <conio.h>
    class B {
      int x;
    public:
      B(){x=0; cout<<"Def_B "<<endl;}
      B(int n){x=n; cout<<"Init_B "<<endl;}
      B(const B y){x=y.x; cout<<"Copy_B "<<endl;}
      int get_x(){return x;}
      ~B(){cout<<"Destr_B"<<endl;}
    };
    class D : public B {
      int y;
    public:
      D(){y=0; cout<<"Def_D "<<endl;}
      D(int n){y=n; cout<<"Init_D "<<endl;}
      D(const D z){y=z.y; cout<<"Copy_D "<<endl;}
      int get_y(){return y;}
      ~D(){cout<<"Destr_D"<<endl;}
    };
    void main()
    { B w1;
      cout<<"w1.x="<<w1.get_x()<<endl;
      B w2(2);
      cout<<"w2.x="<<w2.get_x()<<endl;
      B w3(w2);
      cout<<"w3.x="<<w3.get_x()<<endl;
      B w4=w1;
      cout<<"w4.x="<<w4.get_x()<<endl;
      D q1;
      cout<<"q1.x="<<q1.get_x()<<' '<<"q1.y="<<q1.get_y()<<endl;
      D q2(2);
      cout<<"q2.x="<<q2.get_x()<<' '<<"q2.y="<<q2.get_y()<<endl;
      D q3(q2);
      cout<<"q3.x="<<q3.get_x()<<' '<<"q3.y="<<q3.get_y()<<endl;
      D q4=q1;
      cout<<"q4.x="<<q4.get_x()<<' '<<"q4.y="<<q4.get_y()<<endl;
    }
    //=== Результаты работы ===
    Def_B 		//конструктор B по умолчанию для создания w1.x
    w1.x=0		//значение созданного объекта
    Init_B 		//конструктор B инициализации для создания w2.x
    w2.x=2		//значение созданного объекта
    Copy_B 		//конструктор B копирования для создания w3.x
    w3.x=2		//значение созданного объекта
    Copy_B		//конструктор B копирования для создания w4.x
    w4.x=0		//значение созданного объекта
    Def_B 		//неявный вызов конструктора B для создания q1.x
    Def_D 		//конструктор D по умолчанию для создания q1.y
    q1.x=0 q1.y=0	//значения созданных объектов
    Def_B 		//неявный вызов конструктора B для создания q2.x
    Init_D 		//конструктор D инициализации для создания q2.y
    q2.x=0 q2.y=2	//значения созданных объектов
    Def_B 		//неявный вызов конструктора B для создания q3.x
    Copy_D 		//конструктор D копирования для создания w3.y
    q3.x=0 q3.y=2	//значения созданных объектов
    Def_B 		//неявный вызов конструктора B для создания q4.x
    Copy_D 		//конструктор D копирования для создания w4.y
    q4.x=0 q4.y=0	//значения созданных объектов
    Destr_D		//деструктор D для уничтожения w4.y
    Destr_B		//деструктор B для уничтожения w4.x
    Destr_D		//деструктор D для уничтожения w3.y
    Destr_B		//деструктор B для уничтожения w3.x
    Destr_D		//деструктор D для уничтожения w2.y
    Destr_B		//деструктор B для уничтожения w2.x
    Destr_D		//деструктор D для уничтожения w1.y
    Destr_B		//деструктор B для уничтожения w1.x
    Destr_B		//деструктор B для уничтожения q4.x
    Destr_B		//деструктор B для уничтожения q3.x
    Destr_B		//деструктор B для уничтожения q2.x
    Destr_B		//деструктор B для уничтожения q1.x

    Обратите внимание на то, что в этом примере создание объектов производного класса начинается с автоматического вызова конструктора базового класса по умолчанию. Кроме того, объекты уничтожаются в порядке, обратном последовательности их создания – самый первый объект разрушается последним.

    Однако возможна ситуация, когда ни программист, ни система не включили в базовый класс конструктор по умолчанию. Это происходит в тех случаях, когда программист написал только конструкторы с параметрами. В такой ситуации конструкторы производного класса должны сами позаботиться об инициализации объектов родительского класса. Сделать это можно разными способами – явно вызвать конструктор базового класса либо в своем списке инициализации, либо в теле конструктора. Для защищенных ( protected ) полей базового класса можно воспользоваться указателем this. В приводимом ниже примере демонстрируются эти возможности. В качестве базового класса выступает класс Point2D, моделирующий точку на плоскости:

    class Point2D {
      int x,y;	//закрытые данные класса Point2D
    public:
      Point2D(int xx,int yy):x(xx),y(yy){} //конструктор инициализации
      Point2D(const Point2D P):x(P.x),y(P.y){} //конструктор копирования
      int get_x(){return x;}
      int get_y(){return y;}
    };

    Порожденный класс Point3D моделирует точку в трехмерном пространстве:

    class Point3D: public Point2D {
      int z;    //новая координата в классе Point3D
    public:
      Point3D(int xx,int yy,int zz):Point2D(xx,yy),z(zz){}
      int get_z(){return z;}    //новый метод в классе Point3D
    };

    А теперь протестируем оба класса на следующей программе:

    #include <iostream.h>
    #include <conio.h>
    void main()
    { Point2D P2(1,2);
      Point3D P3(3,4,5);
      cout<<"P3.x="<<P3.get_x()<<" P3.y="<<P3.get_y()<<" 
          P3.z=" <<P3.get_z()<<endl;
      cout<<"P2.x="<<P2.get_x()<<" P2.y="<<P2.get_y()<<endl;
      P2=P3;
      cout<<"P2.x="<<P2.get_x()<<" P2.y="<<P2.get_y()<<endl;
      getch();
    }
    //=== Результат работы ===
    P3.x=3 P3.y=4 P3.z=5
    P2.x=1 P2.y=2
    P2.x=3 P2.y=4

    Производному классу по наследству достались приватные данные – координаты ( x,y ) родительского объекта и общедоступные методы доступа к этим координатам. Поэтому в головной программе мы можем пользоваться этими методами как по отношению к объектам типа Point2D, так и по отношению к объектам типа Point3D. Немного странным кажется оператор присваивания двухмерному объекту P2 значения трехмерного объекта P3. Но происходит вполне естественная операция – те поля, которые являются общими у этих двух объектов, переносятся, а "лишнее" поле P3.z отсекается. Обратная операция P3=P2 была бы ошибочной, т.к. компилятор не "знает", чем следует заполнить поле P3.z.

    Если бы поля ( x,y ) в базовом классе были объявлены как защищенные ( protected ), то их инициализацию в конструкторе производного класса можно было бы выполнить и так:

    Point3D(int xx,int yy,int zz):z(zz)
             { this->x=xx; this->y=yy; }

    15.1.3. Динамическое создание и удаление объектов

    Объявление объектов с использованием конструкторов создает данные, которые существуют до выхода из блока, в котором они появились. Однако иногда объекты могут потребоваться на более короткое время. Такие объекты можно создавать и уничтожать во время работы программы с помощью операторов new и delete:

    class A {...};    //объявление класса
    ..............
    A *ps=new A;      //объявление указателя и создание объекта типа A
    A* *pa=new A[20]; //объявление указателя и создание массива объектов
    ...............
    delete ps;        //удаление объекта по указателю ps
    delete [] pa;     //удаление массива объектов по указателю pa

    Фактически, выполнение оператора new эквивалентно вызову конструктора класса, а обращение к оператору delete на автомате означает вызов деструктора. Создание одиночных объектов может быть совмещено с инициализацией объекта, если в классе предусмотрен соответствующий конструктор:

    A *ptr1=new A(5);//создание объекта и вызов конструктора инициализации

    Массив создаваемых объектов проинициализировать таким же образом нельзя.

    В ранних версиях C++ для создания и уничтожения динамических объектов использовали обращения к функциям malloc ( запрос памяти ) и free ( освобождение памяти ). Неудобство применения этих функций по сравнению с операторами new/delete заключается в том, что для запроса памяти нужно знать количество байт, занимаемых объектом в оперативной памяти. Конечно, это не так уж и сложно – существует функция sizeof, с помощью которой длину объекта можно определить. Второе неудобство заключается в том, что функция malloc выдает указатель типа void* и его еще надо преобразовать к типу указателя на объект класса.

    Довольно распространенная ситуация, которая может оказаться потенциальным источником ошибок, возникает в процессе создания и удаления динамических объектов. Она заключается в том, что после уничтожения объекта, связанного, например, с указателем ps, этот указатель не чистится. Если после удаления объекта сделать попытку что-то прочитать или записать по этому указателю, то поведение программы предсказать трудно. Поэтому достаточно разумным правилом является засылка нуля в указатель разрушаемого объекта:

    delete ps;
      ps=NULL; 	//или ps=0;

    15.1.4. Виртуальные функции

    Виртуальными называют функции базового класса, которые могут быть переопределены в производном классе. В базовом классе их заголовок начинается со служебного слова virtual. В производном классе такая функция продолжает оставаться виртуальной, даже если перед ее заголовком слово virtual опущено. Однако на практике рекомендуют и в производном классе перед заголовком переопределяемой виртуальной функции указывать термин virtual.

    Заголовки виртуальных функций в базовом и производном классах должны быть обязательно идентичными. Поэтому переопределение распространяется только на тело функции. Это позволяет обращаться к виртуальным функциям, не указывая их принадлежность тому или иному классу. Выбором нужной функции управляет тип объекта, заданный явно или неявно – через указатель, который может быть объявлен как указатель родительского класса. Если в производном классе виртуальная функция не переопределяется, то к объектам порожденного класса применяется родительский виртуальный метод.

    Рассмотрим пример, в котором базовый класс B содержит защищенное поле n и отображает его содержимое на экране. Производный класс D1 отображает квадрат доставшегося по наследству поля. Еще один класс D2, порожденный тем же родителем B, отображает куб своего наследства.

    #include <iostream.h>
    #include <conio.h>
    class B {
    public:
      B(int k):n(k){}	//конструктор инициализации
      virtual void show(){cout<<n<<endl;}	//виртуальная функция
    protected:
      int n;
    };
    class D1: public B {
    public:
      D1(int k):B(k){}	// конструктор инициализации
      virtual void show(){cout<<n*n<<endl;}
    };
    class D2: public B {
    public:
      D2(int k):B(k){}	// конструктор инициализации
      virtual void show(){cout<<n*n*n<<endl;}
    };
    void main()
    { B bb(2),*ptr;
      D1 dd1(2);
      D2 dd2(2);
      ptr=bb;
      ptr->show();
      ptr=dd1;
      ptr->show();
      ptr=dd2;
      ptr->show();
      getch();
    }
    //=== Результат работы ===
    2	//результат работы функции B::show
    4	//результат работы функции D1::show
    8	//результат работы функции D2::show

    Обратите внимание на то, что в предыдущем примере адреса объектов производных классов присваиваются указателю, чей тип был связан с объектами базового класса. Такое присвоение можно делать без явного приведения типов. А вот обратное преобразование указателя базового класса в указатель производного класса сопровождается явным преобразованием типа:

    B *bptr;
      D1 dd1(2);
      bptr=dd1;        //вниз по иерархии классов без преобразования
      D1 *dptr;
      dptr=(D1 *)bptr;  //вверх по иерархии с преобразованием типа

    15.1.5. Виртуальные деструкторы

    Когда существует иерархия производных классов, и мы создаем массив динамических указателей на объекты разных производных классов, то при уничтожении такого рода объектов могут возникнуть проблемы. Продемонстрируем это на примере иерархии геометрических фигур: Shape (фигура базового класса), Circle (окружность, производная от Shape ) и Rectangle (прямоугольник, производный от Shape ):

    #include <iostream.h>
    class Shape {
    public:
      Shape();		//конструктор по умолчанию
      ~Shape();	//стандартный деструктор
      virtual void show() {cout <<"Shape"<<endl;
    };
    class Circle: public Shape {
      int xc,yc,r;	//координаты центра и радиус
    public:
      Circle(int x,int y,int R):xc(x),yc(y),r(R) {}	//конструктор
      ~Circle();	//стандартный деструктор
      void show() {cout<<"x="<<xc<<" 
        y="<<yc<<" r="<<r<<endl;
    };
    class Rectangle: public Shape {
      int x1,y1,x2,y2;	//координаты противоположных вершин
    public:
      Rectangle(int ix1,int iy1,int ix2,int iy2):
    		 x1(ix1),y1(iy1),x2(ux2),y2(iy2) {}	//конструктор
      ~Rectangle();	//стандартный деструктор

    Создаем массив указателей на объекты базового класса и присваиваем им адреса динамически создаваемых объектов:

    Shape *ptr_s[2];
      ptr_s[0]=new Circle(20,20,10)
      ptr_s[1]=new Rectangle(20,40,50,50);

    Поработали с временно созданными объектами, и пришла пора их удалить. Попытка сделать это следующим способом ни к чему хорошему не приведет:

    for(int i=0; i<2; i++) delete ptr_s[i];

    Причина заключается в том, что для удаления этих фигур будет вызван деструктор класса Shape (именно на объекты этого класса был объявлен массив указателей ptr_s ). А ресурсы, занятые окружностью и прямоугольником, при этом не будут освобождены. Выход из создавшегося положения довольно простой – надо объявить деструктор базового класса виртуальным ( virtual ~Shape(); ). Тогда автоматически виртуальными станут и деструкторы производных классов (хотя деструкторы и не наследуются). И все проблемы, связанные с утечкой памяти, будут решены.

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

    15.1.6. Чистые виртуальные функции и абстрактные классы

    Чистая виртуальная функция не совершает никаких действий, и ее описание выглядит следующим образом:

    virtual тип name_f(тип1 a1,тип2 a2,...)=0;

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

    Объявим абстрактным класс Shape (Геометрическая Фигура), в состав которого включим две чистые виртуальные функции – определение площади фигуры ( Get_Area ) и определение периметра фигуры ( Get_Perim ).

    class Shape {
    public:
      Shape(){}	//конструктор
      virtual double Get_Area()=0;
      virtual double Get_Perim()=0;
    };
    class Rectangle: public Shape {
      double w,h;	//ширина и высота
    public:
      Rectangle(double w1,double h1):w(w1),h(h1) {}
      double Get_Area() {return w*h;}
      double Get_Perim() {return 2*w+2*h);}
    };
    class Circle: public Shape {
      double r;	//радиус
    public:
      Circle(double r1):r(r1) {}
      double Get_Area() {return M_PI*r*r;}
      double Get_Perim() {return 2*M_PI*r;}
    };

    Если в производном классе хотя бы одна из чисто виртуальных функций не переопределяется, то производный класс продолжает оставаться абстрактным и попытка создать объект (экземпляр класса) будет пресечена компилятором.

    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

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

    При множественном наследовании может возникнуть некоторая неопределенность, связанная с тем, что родительские данные могут попытаться попасть в производный класс несколькими путями. Например, классы 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();
    }
    Вернуться к учебному плану