Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ТП_ЛабРаботы.doc
Скачиваний:
13
Добавлен:
28.09.2019
Размер:
716.8 Кб
Скачать

Наследование данных и методов

Производный (Derived или Child) класс является основой для реализации меха­низма наследования свойств базового (Base или Parent) класса, а также средством подстройки класса к нуждам пользователя. Подкласс (или производный класс) наследует все данные и методы базового класса. В дополне­ние к ним он может приобрести новые данные и методы, не содержащиеся в базовом классе, но необходимые для конкретных целей, преследуемых при со­здании подкласса. Каждый объект производного класса содержит свои собствен­ные копии данных, унаследованных от базового класса. Любой класс может стать базовым, если создать новый класс, объявив его производным от первого.

class BaseClass

{ // Компоненты базового класса

};

class SubClass: public BaseClass

{ // Компоненты производного класса

};

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

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

Таблица. Правила наследования доступа

Тип наследования доступа

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

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

public

public

public

protected

protected

private

недоступны

protected

public

protected

protected

protected

private

недоступны

private

public

private

protected

private

private

недоступны

По умолчанию установлен тип наследования доступа private.

В производном классе можно изменять доступность отдельных компонентов базового класса.

class BaseClass

{

public: // Общедоступные компоненты базового класса

int nPublBase;

void funcBase ();

protected: // Защищенные компоненты базового класса

int nProtBase;

private: // Частные компоненты базового, класса

int nPrivBase;

};

class Subclass: BaseClass // private-наследование по умолчанию

{

public: // Общедоступные компоненты производного класса

BaseClass::nPublBase; // Конкретно объявляем как public

int nPublSub;

void funcSub();

} clSub;

Из любого места программы становится возможен доступ к этому компо­ненту базового класса:

clSub.nPublBase = 7;

Полиморфизм и виртуальные функции

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

Главным моментом в понимании сути полиморфизма является момент време­ни, в который система определяет детали реализации метода для объекта. Если система решает, как выполнить действие во время компиляции, то этот процесс называется ранним связыванием, если решение принимается ди­намически во время исполнения программы, то это называется поздним связыва­нием. Под связыванием понимают связывание сути послания (име­ни метода) с конкретными кодами его реализации для данного объекта.

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

class Parent

{

public:

void print(){…}

} *pp,pobj;

class Derived: Parent

{

public:

void print(){…}

} *pd,dobj;

pobj.print (); // Будут вызваны разные print()

dobj.print(); // Полиморфизм раннего связывания

Компилятор заранее решает, каким классам должны принадлежать функции print в последних двух операторах. Очевидно, что для объекта pobj класса Parent будет вызван метод своего же класса, то есть Parent::print(). Аналогично для производного класса справедлив вызов Derived::print().

В языке C++ указатель на базовый класс может содержать адрес объекта про­изводного класса.

рp=&pobj;

pp->print(); // Вызов метода Parent::print()

рd=&dobj;

pp=pd; // Будут вызваны разные print()

pp->print(); // Вызов метода Parent::print()

pd->print(); // Вызов метода Derived::print()

Указатели рр и pd содержат адрес объекта dobj производного класса. Оператор pd->print(); вызовет метод Derived::print(), так как pd является указателем на класс Derived и содержит в данный момент адрес объекта dobj того же класса. В случае оператора pp->print(); полиморфизм раннего связывания диктует следующее правило. Так как рр объявлен указателем на класс Parent, то оператор pp->print(); вызовет Parent::print(). Тот факт, что рр в данный момент времени содержит адрес объекта производного класса, не влияет на решение, принятое на этапе компиляции.

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

class Parent

{

public:

virtual void print(){…}

} *pp,pobj;

class Derived: Parent

{

public:

void print(){…}

} *pd,dobj;

pobj.print (); // Вызов метода Parent::print()

dobj.print(); // Вызов метода Derived::print()

рp=&pobj;

pp->print(); // Вызов метода Parent::print()

рd=&dobj;

pp=pd;

pp->print(); // Вызов метода Derived::print()

pd->print(); // Вызов метода Derived::print()

В этих условиях изменится функционирование только одного оператора pp->print. В игру вступает полиморфизм позднего связывания, и выбор метода print производит­ся на этапе исполнения программы в зависимости от того, на объект какого класса ссылается в данный момент времени указатель Parent *pp. Если он ссылается на объект производного класса, то будет вызван Derived::print(), если же указатель ссылается на объект базового класса, то будет вызван Parent::print(). Такая гибкость функционирования виртуальных функций явля­ется мощным средством, позволяющим выбрать нужный метод при получении указателем (на текущий объект одного из производных классов иерархии) сооб­щения общего характера вида print(), input() и т. д.

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

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

class Box

{

public: // Виртуальный деструктор

virtual ~Box() { puts(“Box”); }

}

class Menu : public Box

{

public: // ~Menu() тоже виртуальный

~Menu() { puts(“Menu”); }

}

class PullDownMenu : public Menu

{

publiс: // Этот деструктор тоже виртуальный

~PullDownMenu() { puts(“Pull”); }

}

Теперь объявим и заполним массив указателей на базовый класс:

Box *window[3];

window[0] = new Box;

window[l] = new Menu;

window[2] = new PullDownMenu;

Вызовы деструкторов порождают следующие цепочки действий:

delete window[0]; // Будет вызван ~Вох()

delete window[l]; // Будет вызван ~Меnu(), потом ~Вох()

delete window[2]; // Будет вызван ~PullDownMenu(), потом ~Меnu(), и, наконец, ~Вох()

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

При определении в производных классах виртуальных функций количество и типы параметров у всех функций в разных классах иерархии должны быть одина­ковы. Только в этом случае они считаются виртуальными.

Необходимо различать три варианта переопределения функций, для которых в английском языке исполь­зуются различные специальные термины. Говорят, что переопределенная в производном классе виртуальная функция (overrides) преобладает (над) или заменяет одноименную функцию базового класса. Если в производном классе переопреде­лена обычная функция, то говорят, что она скрывает (hides) функцию из базового класса с таким же именем. Если в классе определены две функции с одинаковым именем, но разным набором параметров, то говорят, что они совмещены или что вторая является перегруженной (overloaded) версией первой функции.

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