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

Наследование и другие возможности классов Наследование классов

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

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

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

Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, - производными (порожденными), иногда классами-потомками или наследниками. Производные классы "получают наследство" -данные и методы своих базовых классов - и, кроме того, могут пополняться собственными компонентами (данными и собственными методами). Наследуемые компоненты не перемещаются в производ­ный класс, а остаются в базовых классах. Сообщение, обработку ко­торого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сооб­щения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе (рис. 10.1).

Рис. 10.1. Схема обработки сообщений в иерархии объектов;

1 - обработка сообщения методами производного класса;

2 - обработка сообщения методами базового класса.

Если класс "точка (позиция) на экране" считать базовым классом, то на его основе можно построить класс "окно на экране". Данными этого класса будут две точки:

• точка, определяющая левый верхний угол;

• точка, определяющая размеры окна, т.е. смещения вдоль коор­динатных осей относительно левого верхнего угла.

Методы класса "окно на экране":

• сместить окно вдоль оси х на dx;

• сместить окно вдоль оси y на dy;

• сообщить значение координаты х левого верхнего угла;

• сообщить значение координаты y левого верхнего угла;

• сообщить размер окна вдоль оси х;

• сообщить размер окна вдоль оси Y.

Конструктор окна на экране:

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

Деструктор окна на экране:

• уничтожить окно с заданным именем.

Обратите внимание, что две точки по-разному используются в классе "окно на экране". Первая из них - это абсолютные координаты точки на экране, вторая - интерпретируется просто как пара чисел, определяющая размеры окна. Таким образом, если первая точка имеет координаты (4,3), а вторая (0,0), то это соответствует пустому окну (окну с нулевыми размерами). Наименьшее окно, в которое можно вывести один символ (или один пиксель в графическом режиме), должно иметь размеры (1,1) независимо от положения левого верхнего угла.

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

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

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

При наследовании классов важную роль играет статус доступа (статус внешней видимости) компонентов. Для любого класса все его компоненты лежат в области его действия. Тем самым любая принадлежащая классу функция может использовать любые компонентные данные и вызывать любые принадлежащие классу функции. Вне класса в общем случае доступны только те его компоненты, которые имеют статус public.

В иерархии классов соглашение относительно доступности компонентов класса следующее.

Собственные (private) методы и данные доступны только внутри того класса, где они определены.

Защищенные (protected) компоненты доступны внутри класса, в котором они определены, и дополнительно доступны во всех производных классах.

Общедоступные (public) компоненты класса видимы из любой точки программы, т.е. являются глобальными.

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

Еще раз отметим, что на доступность компонентов класса влияет не только явное использование спецификаторов доступа (служебных слов) - private (собственный), protected (защищенный), public (общедоступный), но и выбор ключевого слова class, struct, union, с помощью которого объявлен класс.

Определение производного класса. В определении и описании производного класса приводится список базовых классов, из которых он непосредственно наследует данные и методы. Между именем вводимого (нового) класса и списком базовых классов помещается двоеточие. Например, при таком определении

class S: X, Y, Z { ... };

класс s порожден классами x,y,z, откуда он наследует компоненты. Наследование компонента не выполняется, если его имя будет использовано в качестве имени компонента в определении производного класса s. Как уже говорилось, по умолчанию из базовых классов наследуются методы и данные со спецификаторами доступа - public (общедоступные) и protected (защищенные).

В порожденном классе эти унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключевого слова class, и статус доступа public, если новый класс определен как структура, т.е. с помощью ключевого слова struct. Таким образом, при определении класса struct J: x, z { ... ); любые наследуе­мые компоненты классов х, z будут иметь в классе J статус общедо­ступных (public). Пример:

clasa B { protected: int t;

public: char u; };

class E: В { ... }; // t, u наследуются как private

struct S: В { ... ); // t, u наследуются как public

Явно изменить умалчиваемый статус доступа при наследовании можно с помощью спецификаторов доступа - private, protected и public. Эти спецификаторы доступа указываются в описании производного класса непосредственно перед нужными именами базовых классов. Если класс в определен так, как показано выше, то можно ввести сле­дующие производные классы:

class M: protected В { ... }; // t, u наследуются как

// protected

class P: public В {...}; // t - protected, и – public

class D: private В { ... }; // t, и наследуются как

// private

struct F: private В {...}; // t, и наследуются как

// private

struct G: public В {...); // t - protected, и - public

Соглашения о статусах доступа при разных сочетаниях базового и производного классов иллюстрирует табл. 10.1.

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

Чтобы проиллюстрировать некоторые особенности механизма на­следования, построим на основе класса point (см. п. 9.4) производный класс spot (пятно). Наследуемые компоненты класса point:

• int x, у - координаты точки на экране;

• point () - конструктор;

• givex (), givey () - доступ к координатам точки;

• show () - изобразить точку;

• move () - переместить точку.

Дополнительно к наследуемым компонентам в класс spot введем: радиус пятна (гad); его видимость на экране (vis — 0, когда изображения нет на экране, vis == 1 - изображение есть на экране); признак сохранения образа в оперативной памяти (tag == 0 - битовый образ не хранится, tag == 1 - битовый образ хранится в памяти); указатель pspot на область памяти, выделенную для хранения битового образа изображе­ния.

Таблица 10.1 Статусы доступа при наследовании

Доступ в базовом классе

Спецификатор доступа перед базовым классом

Доступ в производном классе

struct

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

недоступны

недоступны

// SPOT.CPP - класс, наследующий данные и методы

// класса POINT

#ifndef SPOT

#define SPOT 1

#include "point.cpp" // Определение класса point

class spot:

// 'public1 позволит сохранить статусы доступа для

// наследуемых компонентов класса POINT: public point

{ // Статус доступности данных в производных классах: protected:

int rad; // Радиус пятна (изображения)

int vis; // Видимость пятна на экране

int tag; // Признак сохранения образа в памяти

void *pspot; // Указатель на область памяти для

// изображения (для битового образа)

Доступ в базовом Спецификатор классе доступа перед Доступ в производном классе базовым классом

struct 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 недоступны недоступны

public:

// Конструктор класса SPOT:

spot(int xi, int yi, int ri):

// Вызов конструктора базового класса:

point(xi,yi) { int size;

vis = 0; tag =0; rad = ri; // Определить размеры битового образа:

size = image size (xi-ri,yi-ri,xi+ri, yi+ri) ; // Выделить память для битового образа:

pspot = new char[size]; }

~spot() // Деструктор класса

SPOT { hide(); // Убрать с экрана изображение пятна

tag =0; // Сбросить признак сохранения в памяти

delete pspot; // Освободить память, где находился

// битовый образ }

void show() // Изобразить пятно на экране дисплея

{ // Если битового образа нет в памяти:

if (tag = = 0)

{ // Нарисовать окружность на экране:

circle(х,у,rad); // Закрасить пятно

floodfill(x,y,getcolor()); // Запомнить битовый образ в памяти:

getimage(x-rad,y-rad,x+rad,y+rad,pspot); tag = 1; }

else

// Перенести изображение из памяти на экран:

putimage (x-rad,y-rad,pspot,XOR_PUT) ;

vis = 1; }

void hide() // Убрать с экрана изображение пятка

{ if (via = = 0) // Нечего убирать return;

// Стереть изображение с экрана:

putimage(x-rad,y-rad,pspot,XOR_PUT);

vis = 0; }

// Переместить изображение:

void move(int xn, int yn) { hide(); // Убрать старое изображение с экрана

// Изменить координаты центра пятна:

х = хп; у = уп;

show () ; // Вывести изображение в новом месте

}

// Изменить размер изображения пятна:

void vary(float dr) { float a;

int size;

hide(); // Убрать старое изображение с экрана

tag = 0;

// Освободить память битового образа:

delete pspot;

// Вычислить новый радиус:

а = dr * rad;

if (а <= 0) rad = 0; else rad = (int)a;

// Определить размеры битового образа:

size = imagesize(x-rad,y-rad,x+rad,y+rad);

// Выделить память для нового образа:

new char[size];

show(); // Изобразить пятно на экране

}

int& giver(void) // Доступ к радиусу пятна

{ return rad; }

};

#endif

В классе spot явно определены конструктор, деструктор ~spot() и пять методов: show () вывести на экран изображение пятна, затем перенести его битовый образ в память; hide () убрать с экрана изображение пятна; move () переместить изображение в другое место на экране;

vary () изменить (уменьшить или увеличить) изображение на экране; giver () обеспечить доступ к радиусу пятна.

Из класса point класс spot наследует координаты (х, у) точки (центра пятна) и методы givex(), givey(). Методы point: :show(), point :: move () заменены в классе spot новыми функциями с такими же именами, а функция point : : hide () не наследуется, так как в классе point она имеет статус собственного компонента (private).

Конструктор spot () имеет три параметра - координаты центра (xi, yi) и радиус пятна на экране (ri). При создании объекта класса spot вначале вызывается конструктор класса point, который по зна­чениям фактических параметров, соответствующих xi, yi, определяет точку - центр пятна. Эта точка создается как безымянный объект класса point. (Конструктор базового класса всегда вызывается и вы­полняется до конструктора производного класса.) Затем выполняются операторы конструктора spot (). Здесь устанавливаются начальные значения признаков vis, tag, и по значению фактического параметра, соответствующего формальному параметру ri, определяется радиус пятна rad. С помощью стандартной функции image size () из графи­ческой библиотеки graphics, lib вычисляется объем памяти (вспомогательная переменная size), требуемый для сохранения пря­моугольного (квадратного) участка экрана, на котором предполагает­ся изобразить пятно. Выделение участка основной памяти нужного объема выполняет стандартная операция new, операнд которой - это массив типа char из size элементов. Выделенная память связывается с указателем pspot, имеющим в классе spot статус protected. На этом работа конструктора заканчивается.

В функциях show () - изобразить пятно на экране, varyO - изме­нить размер изображения и hide {) - убрать изображение пятна с эк­рана используются возможности графических функций: circle(х,у,rad)

нарисовать окружность с центром в точке с координатами (х, у) и радиусом rad; floodfill(x,y,c)

закрасить ограниченную область, которой принадлежит точ­ка с координатами (х, у), цветом, определенным параметром с, getcolor () определить текущий цвет изображений;

getimage(xl,у1,х2,у2,pnt)

поместить в заранее выделенный участок основной памяти, связанный с указателем pnt, битовый образ прямоугольного участка экрана, выделенного координатами левого верхнего (xl, yl) и правого нижнего (х2, у2) углов;

putimage(xl,yl,pnt,op)

изобразить на экране битовый образ, ранее сохраненный в памяти с помощью функции getimage О", xl, yl - координаты размещения на экране левого верхнего угла, pnt - указатель на область памяти, где хранится нужное изображение; ор – параметр, определяющий правила выбора цвета для каждого изображаемого пикселя. Выбор цвета осуществляется с учетом имеющегося на экране пикселя и сохраненного в памяти. Па­раметр ор определяет правило сочетания этих цветов в соот­ветствии с табл. 10.2. Необходимо обратить внимание на одну особенность режима xor_put. Если в этом режиме изображе­ние вывести на экран в то же место, где уже было то же самое изображение, то изображение исчезнет с экрана. Именно так убирает с экрана пятно функция hide(). Флажок видимости пятна на экране vis необходим для распознавания необходи­мости повторного применения функции putimage ().

Функция show () выполняется в разных режимах в зависимости от значения признака tag записи изображения в память. Если значение tag равно 0, то рисуется и закрашивается окружность, затем ее образ переписывается в память функцией getimage () и устанавливается в 1 значение tag. Если значение tag равно 1, то образ переносится на эк­ран из той области основной памяти, где он сохранялся,- при помощи функции putimage ().

Действия функции move () понятны из комментариев в тексте про­граммы.

Таблица 10.2

Правила выбора цвета при размещении на экране битового образа с помощью функции putimage ()

Значение параметра ОР

Условное обозначение в graphics.h

Смысл преобразования

0

COPY_PUT

Копия без всяких условий

1

XOR_PUT

Исключающее ИЛИ

2

OR PUT

Включающее ИЛИ (дизъюнкция)

3

AND__PUT

Логическое И (конъюнкция)

4

NOT_PUT

Копия с инверсией изображения

Особенностью функции vary () является необходимость не только изменить размеры изображения, но и заново сохранить его битовый образ в основной памяти. При изменении размеров изображения нуж­но изменять и размеры памяти для его образа.

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

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

~имя_класса

У деструктора не может быть параметров (даже типа void), и деструктор не имеет возможности возвращать какой-либо результат, даже типа void. Статус доступа деструктора по умолчанию public (т.е. деструктор доступен во всей области действия определения класса).

В несложных классах деструктор обычно определяется по умолчанию. Например, в классе point деструктор явно не определен, и компилятор предполагает, что он имеет вид

~point() { };

В классе spot деструктор явно определен:

~spot О { hide(); tag = 0; delete [] pspot; }

Его действия: убрать с экрана изображение пятна, обратившись к функции spot ::hide(); установить в нуль признак tag наличия в памяти битового образа пятна; освободить память, выделенную при создании объекта для битового образа пятна и связанную с конкретным экземпляром указателя pspot.

Деструкторы не наследуются, поэтому даже при отсутствии в производном классе (например, в классе spot) деструктора он не передается из базового (например, из point), а формируется компилятором как умалчиваемый со статусом доступа public. Этот деструктор вызывает деструкторы базовых классов. В рассматриваемом примере это будет выглядеть примерно так :

public: ~spot() { ~point(); }

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

Деструкторы базовых классов выполняются в порядке, обратном перечислению классов в определении производного класса. Таким образом, порядок уничтожения объекта противоположен по отношению к порядку его конструирования.

Вызовы деструкторов для объектов класса и для базовых классов выполняются неявно и не требуют никаких действий программиста. Однако вызов деструктора того класса, объект которого уничтожается в соответствии с логикой выполнения программы, может быть яв-ным. Это может быть, например, случай, когда при создании объекта для него явно выделялась память. Примером целесообразности явного вызова деструктора может служить класс spot.

Объяснив основные принципы работы деструкторов, приведем программу для работы с объектами класса spot:

//Р10-01.СРР - наследование классов и их деструкторы

#include <graphics.h> // Связь с графической библиотекой

#include <conio.h> // Прототип функции getch()

#include "spot.cpp" // Определение класса spot void main ()

{ // Переменные для инициализации графики:

int dr = DETECT, mod;

// Инициализация графической системы:

initgraph(&dr,&mod,"с:\\borlandc\\bgi");

{ //В этом блоке создаются и используются объекты

// класса spot

spot A(200,50,20); // Создается невидимое пятно А

spot D(500,200,30); // Создается невидимое пятно D

A.show(); // Изобразить пятно А на экране

getch(); // Ждать нажатия клавиши

D.show(); // Изобразить пятно D на экране

getch(); A.move(50,60); // Переместить пятно А

getch(); О.vary (3); // Изменить размеры пятна D

getсh(); // Ждать нажатия клавиши

}

// При выходе из блока для каждого объекта автоматически

// вызывается деструктор, освобождающий выделенную

// память

closegraph(); // Закрыть графический режим

} // Конец программы

Изменение состояний экрана при выполнении программы иллюстрирует рис. 10.2.

Принципиальным отличием этой программы от приведенных выше программ р9-04,срр, Р9-11.СРР для работы с объектами класса point является наличие внутреннего блока, что связано с наличием в классе spot деструктора, при выполнении которого вызывается компонентная функция hide(), использующая функции графики. Эти функции могут выполняться только в графическом режиме, т.е. до выполнения функции closegraph()). Если построить программу без внутреннего блока так же, как упомянутые программы с классом point, то деструктор по умолчанию будет вызываться только при окончании программы, когда графический режим уже закрыт и вы­полнение любых графических функций невозможно. Указанной оши­бочной ситуации можно избежать двумя путями: либо вызывать деструктор явно для уничтожения объектов а и d, а потом закрывать графический режим, либо после инициализации графики ввести внут­ренний блок, в котором определены объекты а и d и при выходе из которого они уничтожаются, для чего деструктор дважды вызывается автоматически. Графический режим закрывается во внешнем блоке, когда объекты A, D уже уничтожены и обращения к деструктору ~spot () не нужны. В программе реализовано второе решение.

Рис. 10.2. Последовательность изображений на экране при выполнении программы Р10-01 .СРР

В качестве несложного упражнения можно удалить скобки ‘()’, выделяющие внутренний блок, и убедиться, что при выполнении из­мененной программы будет выдаваться сообщение об ошибке в гра­фической системе:

BGI Error: Graphics not initialized (use 'initgraph')

Второе решение - явный вызов деструкторов без добавления вло­женного блока:

getch(); D.vary(3); // Изменить размеры пятна D

getch(); // Ждать нажатия клавиши

А.spot::-spot(); // Уничтожить объект А

getch(); // Ждать нажатия клавиши

D. spot: :-spot О ; // Уничтожить объект D

closegraph(); // Закрыть графический режим

} // Конец программы

В данном варианте класса spot при уничтожении объекта с по­мощью деструктора его изображение удаляется с экрана функцией hide ().