До сих пор мы знакомились с возможностями классов как средства
создания и обработки новых типов данных. Наряду с этим важнейшим
достижением языка C++ имеется другая, не менее важная заслуга классов,
– они позволяют строить развивающиеся иерархические структуры
программных комплексов. И главным механизмом здесь является наследование – возможность порождать новые классы на базе уже
имеющихся с передачей порожденным классам наследства в виде данных и
членов-функций родительского класса (или классов, если прямых
родителей несколько). Порожденные классы имеют возможность расширять
набор данных, полученных по наследству, модифицировать родительские
методы и функции, создавать новые данные и новые функции их обработки.
Возможность сохранять ранее созданное программное хозяйство,
модифицируя его в соответствии с новыми задачами, позволяет с меньшими
затратами и с большей надежностью вести разработки больших программных
систем. Дополнительный выигрыш в производительности процесса
разработки программного обеспечения можно получить за счет
использования библиотек классов и шаблонов, активно создаваемых в
настоящее время.
Когда говорят о классе D, порожденном из класса B, то принято
называть родительский класс базовым, а вновь созданный класс – производным. Механизм наследования ( inheritance ) предусматривает две
возможности. В первом случае, который называют простым наследованием,
родительский класс один. Во втором случае родителей два или больше, и
соответствующий процесс именуют термином множественное наследование. В
первую очередь мы познакомимся с механизмом простого наследования.
Итак, как формально выглядит процедура объявления производного
класса D и что он получает в наследство от своего родителя – класса B?
class D: [virtual][public|private|protected] B
{тело производного класса};
Служебное слово virtual (виртуальный) используется для
предотвращения коллизий в случае сложного множественного наследования
(по этому поводу см. раздел 15.2). Кроме уровней доступа public
( общедоступный ) и private ( личный ) в классах, создаваемых на базе
структур ( struct ) и настоящих классов ( class ), используется еще один
уровень защиты – protected ( защищенный ). Защищенными данными класса
могут пользоваться функции и методы самого класса,
Уровень доступа в 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, но они по-прежнему будут
доступны, и результат работы программы будет прежним.
Последовательность вызова конструкторов и 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; }
Объявление объектов с использованием конструкторов создает данные,
которые существуют до выхода из блока, в котором они появились. Однако
иногда объекты могут потребоваться на более короткое время. Такие
объекты можно создавать и уничтожать во время работы программы с
помощью операторов 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* и
его еще надо преобразовать к типу указателя на объект класса.
Довольно распространенная ситуация, которая может оказаться
потенциальным источником ошибок, возникает в процессе создания и
удаления
delete ps; ps=NULL; //или ps=0;
Виртуальными называют функции базового класса, которые могут быть
переопределены в 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; //вверх по иерархии с преобразованием типа
Когда существует иерархия 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];
Причина заключается в том, что для удаления этих фигур будет вызван
ptr_s ). А ресурсы, занятые окружностью и
прямоугольником, при этом не будут освобождены. Выход из создавшегося
положения довольно простой – надо объявить virtual ~Shape(); ). Тогда автоматически виртуальными
станут и
Существует практический совет – если в базовом классе хотя бы одна
из функций объявлена виртуальной, то надо сделать
Чистая виртуальная функция не совершает никаких действий, и ее описание выглядит следующим образом:
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;}
};
Если в
О множественном наследовании говорят в тех случаях, когда в
создании
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
};
При наследовании от виртуального класса
Создание достаточно универсальной
Поэтому мы ограничимся лишь демонстрацией простейшей графической
системы, имеющей в своем распоряжении минимальное число графических
объектов – точки, окружности и залитые окружности. Эти объекты можно
будет создавать в оперативной памяти, отображать на экране, делать
невидимыми и перемещать по экрану в заданное место. Более того, для
манипуляций с этими объектами мы воспользуемся существующей в среде BC
3.1 библиотекой процедур
Описания наших новых классов, методов и вспомогательных утилит мы
разместим в файле с именем gs.h (от 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 содержит заголовки функций и описания
констант библиотеки gs, может быть, понадобится в
будущем для индикации готовности gs=1
система открыта для работы с графическими объектами, закрытие
системы сопровождается засылкой нуля в переменную gs ). Для приведения
библиотеки initgraph
и графический драйвер egavga., который мы из соображений удобства
разместим в своем текущем каталоге. Восстановление текстового режима
работы дисплея осуществляется процедурой closegraph из библиотеки open_gs, а для закрытия – к процедуре close_gs (почти полная
аналогия открытия и закрытия файлов).
Описание нашей GO (от
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 устанавливается черный цвет рисования ( ),
а с помощью процедуры 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.
Добавим класс , производный от класса 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 ), и цвет заливки, равный цвету
объекта (значение переменной ). Обе эти установки выполняются
библиотечной процедурой 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++ имеется другая, не менее важная заслуга классов,
– они позволяют строить развивающиеся иерархические структуры
программных комплексов. И главным механизмом здесь является наследование – возможность порождать новые классы на базе уже
имеющихся с передачей порожденным классам наследства в виде данных и
членов-функций родительского класса (или классов, если прямых
родителей несколько). Порожденные классы имеют возможность расширять
набор данных, полученных по наследству, модифицировать родительские
методы и функции, создавать новые данные и новые функции их обработки.
Возможность сохранять ранее созданное программное хозяйство,
модифицируя его в соответствии с новыми задачами, позволяет с меньшими
затратами и с большей надежностью вести разработки больших программных
систем. Дополнительный выигрыш в производительности процесса
разработки программного обеспечения можно получить за счет
использования библиотек классов и шаблонов, активно создаваемых в
настоящее время.
Когда говорят о классе D, порожденном из класса B, то принято
называть родительский класс базовым, а вновь созданный класс – производным. Механизм наследования ( inheritance ) предусматривает две
возможности. В первом случае, который называют простым наследованием,
родительский класс один. Во втором случае родителей два или больше, и
соответствующий процесс именуют термином множественное наследование. В
первую очередь мы познакомимся с механизмом простого наследования.
Итак, как формально выглядит процедура объявления производного
класса D и что он получает в наследство от своего родителя – класса B?
class D: [virtual][public|private|protected] B
{тело производного класса};
Служебное слово virtual (виртуальный) используется для
предотвращения коллизий в случае сложного множественного наследования
(по этому поводу см. раздел 15.2). Кроме уровней доступа public
( общедоступный ) и private ( личный ) в классах, создаваемых на базе
структур ( struct ) и настоящих классов ( class ), используется еще один
уровень защиты – protected ( защищенный ). Защищенными данными класса
могут пользоваться функции и методы самого класса,
Уровень доступа в 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, но они по-прежнему будут
доступны, и результат работы программы будет прежним.
Последовательность вызова конструкторов и 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; }
Объявление объектов с использованием конструкторов создает данные,
которые существуют до выхода из блока, в котором они появились. Однако
иногда объекты могут потребоваться на более короткое время. Такие
объекты можно создавать и уничтожать во время работы программы с
помощью операторов 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* и
его еще надо преобразовать к типу указателя на объект класса.
Довольно распространенная ситуация, которая может оказаться
потенциальным источником ошибок, возникает в процессе создания и
удаления
delete ps; ps=NULL; //или ps=0;
Виртуальными называют функции базового класса, которые могут быть
переопределены в 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; //вверх по иерархии с преобразованием типа
Когда существует иерархия 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];
Причина заключается в том, что для удаления этих фигур будет вызван
ptr_s ). А ресурсы, занятые окружностью и
прямоугольником, при этом не будут освобождены. Выход из создавшегося
положения довольно простой – надо объявить virtual ~Shape(); ). Тогда автоматически виртуальными
станут и
Существует практический совет – если в базовом классе хотя бы одна
из функций объявлена виртуальной, то надо сделать
Чистая виртуальная функция не совершает никаких действий, и ее описание выглядит следующим образом:
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;}
};
Если в
О множественном наследовании говорят в тех случаях, когда в
создании
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
};
При наследовании от виртуального класса
Создание достаточно универсальной
Поэтому мы ограничимся лишь демонстрацией простейшей графической
системы, имеющей в своем распоряжении минимальное число графических
объектов – точки, окружности и залитые окружности. Эти объекты можно
будет создавать в оперативной памяти, отображать на экране, делать
невидимыми и перемещать по экрану в заданное место. Более того, для
манипуляций с этими объектами мы воспользуемся существующей в среде BC
3.1 библиотекой процедур
Описания наших новых классов, методов и вспомогательных утилит мы
разместим в файле с именем gs.h (от 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 содержит заголовки функций и описания
констант библиотеки gs, может быть, понадобится в
будущем для индикации готовности gs=1
система открыта для работы с графическими объектами, закрытие
системы сопровождается засылкой нуля в переменную gs ). Для приведения
библиотеки initgraph
и графический драйвер egavga., который мы из соображений удобства
разместим в своем текущем каталоге. Восстановление текстового режима
работы дисплея осуществляется процедурой closegraph из библиотеки open_gs, а для закрытия – к процедуре close_gs (почти полная
аналогия открытия и закрытия файлов).
Описание нашей GO (от
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 устанавливается черный цвет рисования ( ),
а с помощью процедуры 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.
Добавим класс , производный от класса 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 ), и цвет заливки, равный цвету
объекта (значение переменной ). Обе эти установки выполняются
библиотечной процедурой 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();
}
Для получения официальных документов о завершении программы дополнительного профессионального образования (удостоверения о повышении квалификации, дипломов о профессиональной переподготовке и MBA) необходимо предоставить:
Внимание! Вы можете не заказывать доставку бумажной версии официального документы, а скачать его в электронном виде и распечатать самостоятельно. Информация о выданном документе в течение 1 месяца загружается в Федеральную информационную систему «Федеральный реестр сведений о документах об образовании и (или) о квалификации, документах об обучении» - ФИС ФРДО.
Доступ на новый сайт осуществляется с использованием адреса электронной почты, который был указан вами при регистрации на "старом". Мы постарались перенести все ваши данные с прежнего ресурса, однако не исключена вероятность потери части информации.
При возникновении проблемы со входом, воспользуйтесь функцией сброса пароля
Если вы обнаружите несоответствия, пожалуйста, сообщите нам.