Множественное наследование
Рассмотрим две проблемы, которые возникают при множественном наследовании: конфликт имен между суперклассами и повторное наследование.
Конфликт имен происходит тогда, когда в двух или более суперклассах случайно оказывается элемент (переменная или операция) с одинаковым именем.
Пример. Определим абстракцию "Работающий студент". Для этого введем более общие абстракции "Работник" и "Студент". Абстракция "Работающий студент" будет наследовать компоненты обеих общих абстракций.
class Worker {
public:
int ID_profession; // код профессии
char* Name; // имя
};
class Student {
public:
int ID_university; // код университета
char* Name; // имя
};
class Student_Worker: public Student, public Worker { . . . };
Рассмотрим последовательность действий
Student_Worker He;
. . .
He.ID_profession; // правильно
He.Name; // неправильно – двусмысленно
Конфликт имен элементов подкласса может быть разрешен полной квалификацией имени члена класса, т.е. к именам добавляют префиксы, которые указывают имена тех классов, откуда они пришли.
He.Worker :: Name; // правильно
Повторное наследование возникает тогда, когда при задании более чем одного базового класса какой-либо класс дважды является базовым для другого класса.
Продолжим пример с работающим студентом. Анализируя глубже полученную иерархию наследования, мы обнаружим, что и работник, и студент имеют ряд общих признаков, в частности, имя. Разумно ввести еще более общую абстракцию "Человек".
class Person {
public: char* Name; // имя
}
class Worker : public Person {
public: int ID_profession; // код профессии
}
class Student : public Person {
public: int ID_university; // код университета
}
Наследственная иерархия класса Student_Worker представлена на рис. 4.1.
Рис. 4.1 Наследственная иерархия класса Student_Worker
Для доступа к одной из копий унаследованного элемента необходимо воспользоваться явной квалификацией, т.е. добавить к его имени префикс в виде имени класса-источника.
He.ID_profession; // правильно
He.Name; // неправильно – двусмысленно
He.Person :: Name; // неправильно – двусмысленно
He.Worker :: Name; // правильно
He.Student :: Name; // правильно
Продолжая анализ полученной иерархии, заметим, что работающий студент имеет всего одно имя. В результате объект класса Student_Worker должен использовать единственную копию элемента Name, унаследованную от Person. В результате приходим к ромбовидной структуре наследования для класса Student_Worker, представленной на рис 4.2.
Рис. 4.2 Ромбовидная структура наследования для класса Student_Worker
Одним из механизмов задания такого совместного использования является виртуальный базовый класс. Виртуальный базовый класс в производном классе представлен одним и тем же (совместно используемым) объектом. Для задания виртуального наследования используется синтаксис следующего примера.
class Person { . . .};
class Worker : public virtual Person {. . .};
class Student : public virtual Person {. . .};
class Student_Worker: public Student, public Worker {. . . };
Зависимость
Пример. Пусть управление температурой каждый объект класса Controller осуществляет в соответствии с задаваемым ему планом. План представим в виде экземпляра класса Plan.
class Plan;
class Controller{
. . .
void process (Plan& );
. . .
};
Класс Plan упомянут как часть описания функции-члена process; это дает нам основание сказать, что класс Controller пользуется услугами класса Plan.
Отношение зависимости (использования) между классами означает, что изменение в спецификации одного класса может повлиять на другой класс, который его использует, причем обратное в общем случае неверно. Можно сказать, что один из классов (клиент) пользуется услугами другого (сервера).
Один класс может использовать другой по-разному. В нашем примере это происходит при описании интерфейсной функции. Отношение использования также имеет место, если в реализации какой-либо операции происходит объявление локального объекта используемого класса.
Инстанцирование
Пример. Представим, что нам необходимы стек целых чисел и стек контроллеров, управляющих температурой. Мы могли бы описать два стека:
class IntStack {
int stack[100];
. . .
};
class ControllerStack {
Controller* stack[100];
. . .
};
Другой, более разумный, подход – создать универсальный стек, который мог бы хранить элементы любого нужного нам типа. Для этого мы можем описать стек, содержащий указатели на нетипизированные элементы:
class Stack {
void* stack[100];
. . .
};
Однако это не безопасно с точки зрения типов. Никто не гарантирует нам, что пользователь не поместит в стек элемент одного типа, а взять захочет элемент другого типа.
Для реализации нашей идеи необходимо воспользоваться шаблоном или параметризованным классом. Шаблон служит для построения других классов и может быть параметризован другими классами, объектами или операциями.Использование шаблонов реализует в языке С++ особый тип полиморфизма – параметрический полиморфизм.
template <class Тype> class Stack {
Тype stack[100];
. . .
public:
void push (Тype);
Т рор ( );
. . .
};
Префикс template < class Тype > делает Тype параметром объявления, которому этот префикс предшествует.
Инстанцирование – подстановка фактических параметров шаблона вместо формальных. В результате создается конкретный класс, который может иметь экземпляры.
Объявим нужные нам стеки:
typedef Stack < int > IntStack // синоним класса стеков целых чисел
typedef Stack < Controller* > ControllerStack // синоним класса стеков
// контроллеров
IntStack IS; // стек для целых чисел
ControllerStack CS; // стек для контроллеров
Объекты IS и CS – это экземпляры совершенно различных классов, которые даже не имеют общего суперкласса. Тем не менее они получены из одного параметризованного класса Stack.
Инстанцирование безопасно с точки зрения типов. По правилам C++ будет отвергнута любая попытка поместить в стек или извлечь из него что-либо, кроме целых чисел или указателей на экземпляры класса Controller, соответственно.
В языке С++ можно определять шаблоны не только классов, но и функций. В качестве примера рассмотрим определение шаблона функции, служащей для определения максимального из двух элементов.
template <class Тype > Тype max(Тype x, Тype y){
return (x > y) ? x : y;
};
Теперь мы можем использовать один и тот же шаблон для целых и вещественных чисел.
int i, j, k;
double a, b, c;
. . .
k = max <int> (i, j);
c = max <double> (a, b);
Кроме того, возможно использовать этот шаблон и для объектов некоторого класса, если в нем определена операция ">".