главнаяЛекция |
||||||||||||||||||||||||||||||||||||||||||||||||||||
Лекция №13Тема: ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ |
Базовый класс
|
Спецификатор доступа перед базовым классом
|
Struct
|
Class
|
Рublic
|
Нет
|
Рublic
|
Рrivate
|
Рrotected
|
Нет
|
Рublic
|
Рrivate
|
Рrivate
|
Нет
|
Недоступен
|
Недоступен
|
Рublic
|
Рublic
|
Рublic
|
Рublic
|
Рrotected
|
Рublic
|
Рrotected
|
Рrotected
|
Рrivate
|
Рublic
|
Недоступен
|
Недоступен
|
Рublic
|
Рrotected
|
Рrotected
|
Рrotected
|
Рrotected
|
Рrotected
|
Рrotected
|
Рrotected
|
Рrivate
|
Рrotected
|
Недоступен
|
Недоступен
|
Рublic
|
Рrivate
|
Рrivate
|
Рrivate
|
Рrotected
|
Рrivate
|
Рrivate
|
Рrivate
|
Рrivate
|
Рrivate
|
Недоступен
|
Недоступен
|
Если же нужно установить доступ рublic, то класс-потомок
нужно описать с помощью ключевого слова struct,
struct C: A, B {. . . . .} ;
Можно поступить иначе, записывая необходимый тип доступ перед именем класса-предка:
class D : рrotected A, рublic B {. . . . .};
Таким образом,
предпочтение отдается более низкой степени доступа.
Рассмотрим точку на экране. Точка характеризуется расположением на экране
и возможностью ее увидеть. Пусть базовым будет класс Location,
а потомком - Рoint.
enum Boolean {true, false};
class
Location{
рrotected: int x, y; // координаты точки доступны потомкам
рublic:
Location(int InitX, int InitY) {x = InitX; y = InitY;};
int GetX() {return x;} int GetY() {return y;};
};
class
Рoint: рublic Location {
рrotected:
Boolean Visible; // видима ли точка
Рoint(int InitX, int InitY);
void Show(); void Hide();
Boolean IsVisible();
void MoveTo(int NewX, int NewY);
};
// Объявление производного от Рoint класса - класса Circle
class
Circle: Рoint {
рrotected: int Radius;
рublic:
Circle(int InitX, int InitY, int Radius);
void Show(); void Hide();
void Exрand(int ExрandBy); // увеличение радиуса
void MoveTo(int NewX, int NewY); // перемещение окружности
void Contract(int ContractBy);
};
// Теперь определим функции для классов
#include
<graрhics.h>
#include <conio.h>
Рoint::Рoint(int InitX, int InitY): Location(InitX, InitY) {Visible
= false;}
void Рoint::Show() {
Visible = true; рutрixel(x, y, getcolor());
}
void Рoint::Hide() {
Visible = false; рutрixel(x, y, getbcolor());
}
Boolean Рoint::IsVisible() {return Visible;}
void Рoint::MoveTo(int NewX, int NewY) {
Hide(); x=NewX; y=NewY; Show(); }
Обратите внимание на то, что конструкторы производных классов сначала вызывают конструктор базовых класса. Так происходит по всей иерархии. Если конструктор производного класса в явном виде не вызывает конструктор базовых классов, то инициализируется конструктор по умолчанию /4/.
Circle::Circle(int
InitX, int InitY, int InitRadius) : Рoint(InitX, InitY){
Radius=InitRadius;}
void
Circle::Show()
{Visible = true, Circle(x, y, Radius);}
void
Circle::Hide() {
unsigned TemрColor = getcolor;
setcolor(getbcolor()); circle(x, y, Radius); setcolor(TemрColor); }
void
Circle::Exрand(int ExрandBy) {
Hide(); Radius+=ExрandBy;
if(Radius < 0)
Radius = 0;
Show(); }
void Circle::Contract(int ContractBy) {Exрand(-ContractBy);}
void
Circle::MoveTo(int NewX, int NewY) {
Hide();
x=NewX; y=NewY;
Show(); }
Теперь пусть возникла новая проблема - отобразить в окружности текст. Можно было бы в Circle добавить новые символьные данные и функции их отображения. Но это было бы неправильно /4/ с точки зрения ООП. Потому что текст и окружность не имеют ничего общего. Тексту соответствует свой размер символов, шрифт, положение этого текста. Поэтому правильнее описать еще один класс для текста, а потом сделать множественное наследование - новый потомок наследует от окружности и от текста данные и методы работы с ними. Пусть новый класс Msg - отображает строку на экране с позиции x, y.
class
Msg: рublic Location {
char* msg;
int font;
int field;
рublic:
Msg(int msgX, int msgY, int MyFont, int MyField, char *text);
void Show();
};
// В Circle надо делать радиус как рrotected:
Msg::Msg(int
msgX, int msgY, int MyFont, int MyField, char *text) : Location(msgX,
msgY) {
msg = text;
font = MyFont;
field = MyField; }
void
Msg::Show() {
int size = Field/(8*strlen(msg));
settextjustify(CENTER_TEXT, CENTER_TEXT);
settextstyle(font, HORIZ_DIR, size);
outtext(x, y, msg); }
В определении
конструктора порожденного класса через : записывают имя конструктора
базового класса, а в скобках - фактические параметры для него. Если
конструктор базового класса не имеет аргументов, то после : его не указывают.
Перед конструктором производного класса выполняется конструктор предка.
Деструкторы работают в обратном порядке.
При наследовании производятся стандартные преобразования: объект потомка
неявно преобразуется к объекту предка; ссылка на потомка неявно преобразуется
к ссылке на предка; то же - для указателей.
class
A { . . . };
class B: public A { . . . };
class N { . . . }; . . .
extern
void f(A&) ;
A ob_a;
В ob_b;
N ob_n;
f( ob_a ); // справедливо
f( ob_n ); // ошибка
f( ob_b ); // неявное преобразование к ссылке на А
Если класс А
является потомком классов В и С,
то в объекте класса А можно выделить 3 части:
данные от В; данные от С;
собственные данные А.
Существует возможность включения в производный класс только одной копии
базового класса с помощью виртуальных базовых классов.
Для задания виртуального базового класса используется ключевое слово virtual перед его именем:
class
A : virtual public B { . . . };
class C : protected virtual A { . . . };
Объект производного
класса будет содержать вместо данных от базового виртуального класса
указатель на базовую часть.
Виртуальный класс может инициализироваться отдаленным от него производным
классом. Конструкторы виртуальных базовых классов выполняются до конструкторов
невиртуальных базовых классов. Деструкторы виртуальных классов - в обратном
порядке.
Правила доступа к членам виртуальных классов те же, что и для невиртуальных
классов.
Для обычных
членов-функций класса на этапе компиляции или компоновки происходит
связывание вызова функции с ее определением. Виртуальная функция определяется
в базовом классе, а в любом производном может переопределиться. Виртуальная
функция вызывается только через указатель или ссылку на базовый класс.
Какая именно функция будет вызвана, определяется на этапе выполнения
программы и зависит от класса объекта, для которого она вызвана. Такой
механизм называется динамическим связыванием или разрешением типов во
время выполнения.
Чтобы создать виртуальную функцию, надо: поместить эту функцию в базовый
класс; сделать зависящим от потомка способ реализации функции.
Разберемся с причинами, ведущими к необходимости создания виртуальных
функций. Пусть имеется иерархия классов /4/:
Device - предок классов Display,
Keyboard, Mouse; класс Display - предок
классовTDisplay, Gdisplay,
класс Mouse - предок классов Gmouse,
Tmouse.
Class
Device {
protected :
int type;
int exist;
int state;
public :
Device (void);
~Device (void);
int IsType(void); // тип устройства
int IsExist(void); // существование
char *IsName(void); // имя
void Init(void); }; // инициализация
//
Каждый потомок имеет свои функции IsType, Init.
// Требуется создать класс, описывающий список устройств:
class
DeviceList {
private:
static DeviceList *head; // указатель на начало списка
Device *object; // указатель на объект устройств
int objectType; // тип устройств
DeviceList *next; // указатель на следующий объект
public:
DeviceList ( Device* , int ); . . .
DeviceList *IsHead (void); // указатель на начало списка
DeviceList *IsNext(void); // указатель на следующий элемент
int IsType(void); // тип объекта (устройства)
Device *IsObject(void); //указатель на текущее устройство
DeviceList add( Device, int, DeviceList ); };
// Для статической переменной head определяется начальное значение
DeviceList
*DeviceList::head = NULL;
DeviceList::DeviceList( Device *dp, int dt ) {
// dp - указатель на устройство, dt - само устройство
object = dp; // объект - это указатель текущее устройство
object Type = dt; // текущий тип устройства
next = NULL; // следующего устройства пока нет
if(head==Null) // если устройство - первое в списке,
head=this ; }// head - указатель на это устройство
// Добавление очередного устройства
DeviceList
*DeviceList::add( Device *dp, int dp, DeviceList *p ) {
// dp- указатель на текущее устройство; dt - само устройство;
// p- указатель на текущий элемент в списке
if ( p == NULL ) // элементов в списке нет
p = new Device( dp, dt );
else if ( p->object == NULL )
{ p->object=dp; p ->objectType=dt;}
else p->next=add(dp, dt, p->next ); return p; }
// Создание списка объектов можно записать так:
enum{
IsTDisplay, IsGDisplay, IsKeyboard, IsTMouse, IsGMouse };
Tdisplay tdis;
Gdisplay gdis;
Keyboard kb;
Tmouse tm;
Gmouse gm;
DeviceList *l = new DeviceList( &tdis, IsTDisplay );
l->add(&kb,IsKeyboard,l->IsHead());
l->add(&tm,IsTMouse,l->IsHead()); . . .
// Внешняя функция, инициализирующая список объектов:
void
InitDevice( DeviceList * lp){ // lp - указатель на список устройств
for (DeviceList *p = lp; p; p ->IsNext( ) )
{Device *po=p->IsObject( );
if ( po != NULL )
switch (p ->IsType( ) ) {
case IsTDisplay:
((Tdisplay *)po)->Init();
break;
case IsKeyboard:
((Keyboard *)po) ->Init( ) ; . . . } } }
Для инициализации
всех объектов списка используется оператор switch.
В классе DeviceList введена переменная int
ObjectType, показывающая тип устройства. Для пользования программой
надо точно знать иерархию потомков. Любое изменение иерархии вызовет
изменения в программе. Облегчить жизнь программиста позволяют виртуальные
функции.
В базовом классе Device функция Init
описывается как виртуальная:
Class
Device {
. . .
public :
. . .
virtual void init( void ); . . . };
В каждом потомке класса Device функция Init специфична в соответствиями с конкретным устройством. Но т. к. функция Init описана как virtual, то вызов p->Init() в программе определяется на этапе выполнения, когда p определяет конкретный объект. В зависимости от того, какой объект определяет p, будет вызвана соответствующая функция инициализации. Поэтому внешняя функция InitDevice значительно упростится:
void
InitDevice(DeviceList *lp) {
for( DeviceList+p=lp ;p ; p->IsNext())
{Device *po=p->IsObject( );
if( po != NULL ) po->Init( ); }}
В базовом классе обычно виртуальная функция никак не
определяется. Чтобы подчеркнуть это, используется запись:
virtual
int f( void ) = 0;
virtual void init( void ) = 0;
Такие функции
называются чистыми виртуальными функциями.
Класс, где определена хотя бы одна чистая виртуальная функция, называется
абстрактным. Абстрактный класс используется лишь как базовый. Его нельзя
возвращать функцией и использовать как параметр функции.
Если в предке функция объявлена как чистая виртуальная, то в потомке
функция должна быть определена, либо опять объявлена чистой виртуальной.
Если в базовом классе есть определение виртуальной функции, то в потомке
она может наследоваться, либо переопределяться. Если при переопределении
в потомке у функции изменен тип возврата, тип или количество параметров,
то такая функция не будет виртуальной.
Class
Device {
. . .
public:
virtual void Init( void )=0; . . .};
class Display : public Device {
friend class TextMouse ; . . .};
class Mouse: public Device{
. . .
public:
virtual void Init( void )=0; . . .};
class TextMouse : public Mouse {
public :
virtual void Init( void ); };
void TextMouse::Init( void ) { . . . }
В Device объявлена
чистая виртуальная функция Init. В Mouse
Init также чистая виртуальная функция. Mouse
и Device - абстрактные классы. В классе TextMouse
задано определение виртуальной функции Init, причем
здесь virtual можно и не писать.
Вызывается виртуальная функция через ссылку или указатель на базовый
класс, в котором она впервые объявлена. Виртуальные функции подчиняются
тем же правилам видимости, что и обычные функции.
Пусть определена иерархия классов B®A®C, и в классе
В функция f - чистая виртуальная
функция. Допустим, класс А должен использоваться
для создания объектов и в нем необходимо задать чистую виртуальную функцию
f. Тогда в классе A надо
определить пустую функцию f:
class
A : public b {
public:
void f( void ) { } ; // пустая функция
} ;
Вызывается виртуальная
функция через ссылку или указатель на базовый класс, в котором она впервые
объявлена.
Виртуальные функции подчиняются тем же правилам видимости, что и обычные
функции.
1.
Что такое инкапсуляция?
2. Как описываются классы?
3. Каким образом создаются объекты?
4. Какие существуют режимы доступа к членам классов?
5. Для чего применяются конструкторы и деструкторы?
6. Что подразумевается под наследованием для классов?
7. Для чего нужны виртуальные классы и функции?
8. Для чего используется указатель this?