-
Notifications
You must be signed in to change notification settings - Fork 0
CPP 07. Множественное наследование. Прямая и косвенная базы. Виртуальное наследование. Понятие доминирования. Порядок создания и уничтожения объектов. Проблемы множественного наследования. Неоднозначности при множественном наследовании.
ООП использует рекурсивный дизайн – постепенное разворачивание программы от базовых классов к более специализированным. С++ один из немногих языков с множественным наследованием. Оно может упростить граф наследования, но также создает пучок проблем для программиста: возникает неоднозначность, которую бывает тяжело контролировать.
Преимущества множественного наследования:
Какие преимущества множественного наследования? Пойдем методом от противного - попробуем избавиться от множественного наследования, и представим, что было бы.
- Представим такую ситуацию: выстраиваем вертикальную иерархию, класс C наследуется от класса B, а B наследуется от класса A. В этом случае, в класс A мы должны вынести много того, что к понятию класса А не относится. Не совсем логично.
В случае со множественным наследованием (рис. ниже), мы четко разделяем понятия A и B. Такой подход уменьшает иерархию.
- Второй момент (опять не используем множественное наследование). Мы можем не выносить что-то в базовый класс, а один из классов включить как подобъект, то есть не использовать наследование. Пусть С - производная от класса А и включает подобъект класса В. Тоже возникнет проблема - не будем иметь доступа к защищенным полям класса Б (нам придется делать это через методы класса Б) + придется протаскивать интерфейс класса Б для класса С.
Прямая база - класс, от которого мы наследуемся. При наследовании может входить только 1 раз.
Косвенная база - прямая база прямой базы. При наследовании - сколько угодно раз.
Может возникнуть ситуация, когда в наш класс косвенная база входит два раза.
Надо свести ее к ромбовидной схеме наследования.
В большинстве случаев нам необходимо, чтобы базовый класс входил в производный только один раз. Рассмотрим пример ниже, где базовый класс войдёт в производный 2 раза:
Пример:
class A
{
public:
A (char* s) { cout << "Creature A" << s << ";" << endl; }
};
class B : public A
{
public:
B() : A (" from B") { cout << "Creature B;" << endl; }
};
class C : public B, public A // В класс C подобъект класса А будет
{ // входить два раза.
public:
C() : A(" from C") { cout << "Creature C;" << endl; }
};
void main()
{
C obj;
}
Вызовется конструктор класса С. Из С вызовется конструктор класса B (так как класс B наследуется раньше класса A). Вызовется конструктор класса A. Создастся объект класса А. Создастся объект класса B. Из С вызовется конструктор класса А. Создастся объект класса А. Создастся объект класса С.
-
На экран в результате работы программы будет выведено следующее:
Creature A from B; Creature B; Creature A from C; Creature C;
Проблема решается с помощью виртуального наследования.
В большинстве случаев необходимо, чтобы базовый класс входил в производный только один раз. Для этого используется виртуальное наследование.
При виртуальном наследовании меняется порядок создания объекта: если в списке наследования есть виртуальное наследование (виртуальные базы), они отрабатывают в первую очередь слева направо, а потом всё остальные базы.
Чтобы сделать родительский (базовый) класс общим, используется ключевое слово virtual
в строке объявления дочернего класса.
class A{};
class B: virtual public A{};
class C: virtual public A{};
class D: public B, public C{};
Если убрать первый virtual, то класс A будет рассматриваться как подобъект класса D. И подобъект будет создаваться для класса D, потом будет создан подобъект класса В, а для подобъекта класса C создастся подобъект класса A, и потом создастся объект класса C и D.
Если мы не напишем второй virtual, то подобъект класса A не будет создаваться для класса D.
Если мы напишем два virtual, то подобъект класса A создастся только для класса D.
Конструкторы виртуальных баз вызываются в первую очередь. Методы, определяемые в производных классах, доминируют над методами базовых классов. То есть они их подменяют. Для решения этой проблемы можно использовать using.
- конструкторы виртуальных базовых классов выполняются до конструкторов не виртуальных базовых классов, независимо от того, как эти классы заданы в списке порождения;
- если класс имеет несколько виртуальных базовых классов, то конструкторы этих классов вызываются в порядке объявления виртуальных базовых классов в списке порождения;
ВАЖНО! Используя множественное наследование, надо стараться виртуально наследоваться по всем ветвям, чтобы не зависеть от порядка наследования.
При одинаковых именах, доминировать будет метод производного класса (В) над базовым (А)
B obj;
obj.f(); // вызовется этот
obj.f(1); // error, т.к. метод базового класса подменили методом
// производного
-
Множественный вызов методов
Рассмотрим следующую схему:
Предположим, есть объект класса А с методом draw()
, который умеет себя нарисовать. Производные от него классы - B и C, тоже имеют метод draw()
и тоже могут себя нарисовать, а так же они могут нарисовать подобъект базового класса. То есть, при рисовании объектов класса B (аналогично для C) вызывается метод draw()
класса А.
Когда мы создаем объект класса D, в котором мы должны отрисовать объект класса B и объект класса C, draw()
класса A вызывается два раза. Это называется проблема множественного вызова базового класса.
-
Пример. Множественный вызов методов
class A { public: void f() { cout<<"Executing f from A;"<<endl; } }; class B : virtual public A { public: void f() { A::f(); cout<<"Executing f from B;"<<endl; } }; class C : virtual public A { public: void f() { A::f(); cout<<"Executing f from C;"<<endl; } }; class D : virtual public C, virtual public B { public: void f() { C::f(); B::f(); cout<<"Executing f from D;"<<endl; } }; void main() { D obj; obj.f(); }
Метод f()
класса А срабатывает дважды.
- Программа выведет на экран:
Executing f from A;
Executing f from C;
Executing f from A;
Executing f from B;
Executing f from D;
Красивых способов борьбы с этой проблемой, к сожалению, нет.
Пример. Решение проблемы множественного вызова методов
Идея: разделить метод на две части. Часть, которая относится непосредственно к самому классу, и часть, которая относится ко всему объекту.
В классе А мы разделили f()
на две части, причем, то что относится к самому классу A - его собственное, мы делаем его защищённым, доступным только для методов класса А и производных классов. В производных классах мы тоже разделяем - метод, относящийся непосредственно к самому классу и ко всему объекту.
-
Пример:
class A { protected: void _f() { cout<<"Executing f from A;"<<endl; } public: void f() { this->_f(); } }; class B : virtual public A { protected: void _f() { cout<<"Executing f from B;"<<endl; } public: void f() { A::_f(); this->_f(); } }; class C : virtual public A { protected: void _f() { cout<<"Executing f from C;"<<endl; } public: void f() { A::_f(); this->_f(); } }; class D : virtual public C, virtual public B { protected: void _f() { cout<<"Executing f from D;"<<endl; } public: void f() { A::_f(); C::_f(); B::_f(); this->_f(); } }; void main() { D obj; obj.f(); }
-
Программа выведет на экран:
Executing f from A; Executing f from C; Executing f from B; Executing f from D;
Решение не самое красивое, но других, к сожалению, нет. В современных ЯП избавились от множественного наследования. И нам в C++ желательно отказаться, хотя оно иногда помогает реализовать некоторые паттерны.
- Неоднозначность при множественном наследовании.
-
тык
class A { public: int a; int (*b)(); // Если что, это указатель на функцию :) int f(); int f(int); int g(); }; class B { int a; int b; public: int f(); int g; int h(); int h(int); }; class C: public A, public B {}; class D { public: static void fun(C& obj) { obj.a = 1; // Error!!! obj.b(); // Error!!! obj.f(); // Error!!! obj.f(1); // Error!!! obj.g = 1; // Error!!! obj.h(); obj.h(1); // Только для тех методов, которые идут по одной ветви - всё корректно. } }; void main() { C obj; D::fun(obj); }
Есть два класса – класс А и класс В. Класс С – производная от классов А и В. В классе С мы получаем доступ к членам объекта класса C. Здесь играет следующее правило проверки на неоднозначность: проверка на неоднозначность происходит до проверки на перегрузку, на тип и до проверки на уровень доступа.
Решение проблемы (тоже некрасивое решение): Объединяем два класса, и в производном классе полностью подменяем то, что находится в базовых классах. Получается громоздкий производный класс (ведь мы всё, что относится к базовым классам - подменяем). Когда мы объединяем два класса одним классом, есть еще один недостаток.(см. пример ниже).
Неоднозначности при множественном наследовании
Также есть еще один недостаток – когда мы программируем, для объекта какого-либо класса мы должны выделить цель - для чего мы создаем это понятие. Объекты данного класса должны выполнять определенную задачу, и она должна быть только одна.
Правило: У одного объекта не должно быть несколько обязанностей. Когда мы объединяем два понятия, это чаще всего приводит к тому, что новое сформированное понятие имеет несколько обязанностей. Такого быть не должно. Если один объект выступает в разных ролях, мы не должны объединять интерфейс, должны его разносить.
Исключением является ситуация, когда мы объединяем два разных понятия, формируя интерфейс одной обязанности. В этом случае используется следующая схема. У нас есть два класса, и мы формируем новое понятие, используя интерфейс только одного класса. В данном случае идёт наследование только по схеме public
только от класса B, от класса A по схеме private. Таким образом, для объектов класса C интерфейс класса A невидим.
-
Пример:
class A { public: void f1() { cout<<"Executing f1 from A;"<<endl; } void f2() { cout<<"Executing f2 from A;"<<endl; } }; class B { public: void f1() { cout<<"Executing f1 from B;"<<endl; } void f3() { cout<<"Executing f3 from B;"<<endl; } }; class C : private A, public B {}; class D { public: void g1(A& obj) { obj.f1(); obj.f2(); } void g2(B& obj) { obj.f1(); obj.f3(); } }; void main() { C obj; D d; // obj.f1(); Error!!! Неоднозначность // d.g1(obj); Error!!! Нет приведения к базовому классу при наследовании по схеме private d.g2(obj); }
Но здесь здесь есть проблема – проверка на неоднозначность происходит до проверки на схему наследования. Поэтому метод f()
для объектов класса C мы вызвать не сможем - это неоднозначность, хотя наследуем по разной схеме.
Что нужно сделать: Нужно в классе С подменить те методы, которые идут по ветви public
.
При такой схеме (когда в одном случае мы поддерживаем только один интерфейс) множественное наследование можно использовать.
ВАЖНО! Если нет общей базы (общая база задает интерфейсные методы для производных классов), то подмены должны осуществляться только по одной ветке.