Множественное наследование (Multiple inheritance)— наследование от нескольких базовых классов одновременно.
Интерфейсы
Интерфейс — специальные классы, описывающие набор методов, но не имеющие
данных и реализации. Например интерфейсами являются классы:
Наследование от нескольких полноценных классов
Допустим у нас есть классы Cow и Sniper, а мы хотим получить класс, который
обладает данными и методами обоих классов. Назовем его CowSniper. Таким образом
у нас есть возможность «скрещивать» классы.
Таким образом синтаксически множественное наследование почти не отличается от
обычного.
class CowSniper: public Cow, public Sniper {
};
Заметим , что важен порядок перечисления предков, потому как в
зависимости от него:
Перекрытие имен функций
эта проблема есть как и в обычном наследовании, так и в
множественном.
Пусть в классах Cow и Sniper были методы sleep().
Тогда код:
CowSniper cs;
cs.sleep(); // 2
В строке 2 произойдет ошибка компиляции, т. к. компилятор не может
выбрать какой
метод sleep() ему вызывать (от Cow или от Sniper). Поэтому необходимо
сообщимть ему правильный выбор: cs.Cow::sleep();
Перекрытие виртуальных функций
Теперь рассмотрим тот
случай, если метод sleep() виртуальный в классах-предках.
class Cow {
public:
virtual void sleep() = 0;
};
class Sniper {
public:
virtual void sleep() = 0;
};
class CowSniper: public Cow, public Sniper {
public:
virtual void sleep() {}
};
В таком случае будут перегружены сразу оба метода (ведь у них одинаковая сигнатура). Подробнее о том почему это происходит написано в следующем пункте. Тогда в следующих строках кода:
Cow & c = cs;
Sniper & s = cs;
c.sleep(); //3
s.sleep(); //4
В строках 3 и 4 вызовется один и тот же метод CowSniper::sleep();
С одной стороны это удобно, т. к. если методы называются одинаково,
то скорее всего они делают что-то похожее. Тогда перегрузив один метод мы
перегрузим сразу оба.
С другой стороны может возникнуть проблема, если
класс-потомок должен реализовывать один и тот же виртуальный метод от
нескольких базовых классов по-разному. Тогда такая перегрузка будет вредна,
но стандартно по другому не поступить. В таких случаях используется следующий
способ обойти это ограничение.
Если есть иерархия классов A,B,C. И в
классах A и B есть некоторый виртуальный метод f()
.
Добавим в эту иерархию еще два класса A1
и B1. В классах A1 и B1 создадим методы fA()
и fB()
соотвественно. Теперь в C будут методы fA()
и fB()
,
которые необходимо перегрузить, причем код в них разный ;)
class A{
public:
virtual void f();
};
class B{
public:
virtual void f();
};
class A1: public A{
public:
virtual void f() {fA();}
virtual void fA() = 0;
};
class B1: public B{
public:
virtual void f() {fB();}
virtual void fB() = 0;
};
class C: public A1, public B1 {
public:
virtual void fA();
virtual void fB();
};
Теперь использовать эти классы можно так:
C c;
A & a = c;
B & b = c;
a.f(); //5
b.f(); //6
В строке 5 вызовется C::fA()
, а в строке 6 -
C::fB()
. Обратите внимание на то, как перегружен метод
f()
в классах A1 и B1, которые ко всему прочему теперь являются
абстрактными и невозможно создать их экземпляры. Цель достигнута.
Представление объекта в памяти
а) Как мы помним, в случае линейного наследования распределение
полей классов для объекта класса наследника в памяти будет такое:
При этом если мы создадим три указателя:
C * = new C();
B * b = c;
A * a = c;
То указывать они все будут на начало объекта в памяти:
и при этом все хорошо.
б) Теперь перейдем к рассмотрению
простейшего примера множественного наследования:
При этом если создать объект класса C, то в памяти он будет выглядеть
так:
Опять попробуем создать три указателя как и в случае линейного
наследования:
C * = new C();
B * b = c;
A * a = c;
Однако теперь указатели
распределятся следующим образом:
Мы видим, что классы A и B фактически
разделены, но ведь если они имеют
виртуальные методы, то должны вызываться
перегруженные виртуальные методы
класса C. Для того, чтобы это происходило
в каждом из них есть ссылка на таблицу
виртуальных функций. Тогда получается
что в классе C будет сразу два указателя
на две различные таблицы виртуальных
функций (кол-во указателей = кол-ву полиморфных предков).
Получается, что таблиц виртуальных функций две. Это не совсем
так, т. к. они просто лежат рядом в
памяти, т. е. фактически таблица одна,
но в ней могут быть повторения, например, виртуальный деструктор:
в) Теперь рассмотрим множественного
наследования, если в графе наследования
есть цикл:
В памяти объект
класса D будет представлен так:
Возникает проблема дублирования полей класса A, т.к. фактически будет два
разных объекта типа A. Значит при компиляции следующего кода произойдет ошибка,
т.к. непонятно к какому полю обращаться.
Для того, чтобы указать, какой из классов A выбрать следует написать так:
D * d = new D();
A * a = d;
При этом если мы хотим использовать методы класса A, так же необходимо явно
указывать которого их них. Вследствие этого даные в двух объектах A в пределах
одного D могут стать различными.
D * d = new D();
A * a = static_cast<B*> (d);
Рассмотрим два случая такого
наследования и посмотрим на возможные проблемы с наличием двойного объекта.
1) Такой эффект может быть полезен, если класс A является файловым потоком,
а B и C это writer и reader соответственно. А класс D читает из С и пишет в B.
Очевидно, что у B и C файлы могут быть различны (скорее всего так и есть).
2) Рассмотрим умный указатель (A = LinkCounter), который внутри себя
сдержит счетчик. В таком случае в классе D возникает два счетчика, что может
привести к печальным последствием, если в одном месте работать с одним из
них, а в другом с другим.
Вообще такое наследование (с циклами в графе
родства) называется бриллиантовым (diamond inheritance)
или Ромбовидным.
Виртуальное
наследование.
Рассмотрим его на
примере следующей иерархии
Синтаксически виртуальное
наследование почти не отличается от
множественного:
class A{
int k;
};
class B: public virtual A {
};
class C: public virtual A {
};
class D: public B, public C {
};
При этом следующий код
будет прекрасно работать:
А все потому, что классы будут выглядеть следующим образом:
D * d = new D();
A * a = d;
Здась может появиться проблема преобразования указателя на D к указателю на
C. Но этой проблемы нет. Необходимо разобраться каким образом это реализуется.
Дело в том, что при виртуальном наследовании добавляется виртуальная
функция, возвращающая указатель на A. Фактически для программиста она не видна.
Для пояснения обозначим ее за getA(). Стоит заметить что она будет различна в
классах B,C и D.
Теперь при обращении к полю из A (допустим в нем поле
int k
) код вида k = 10;
будет автоматически
преобразован в getA()->k = 10;
.
Теперь рассмотрим очередность вызова конструкторов.
Логично предположить, что вызов конструкторов пройдет так
Но возникает проблема, ведь конструкторы B и C могут вызывать различные
конструкторы A и с различными параметрами.
Для определенности было введено следующее правило: Конструктор A должен
быть явно вызван в конструкторе D, при этом в конструкторах B и C вызов
конструктора A опустится.
В связи с этим есть замечание: Нужно следить и понимать, что при
виртуальном наследовании в конструкторах B и C может не вызваться конструктор A
с разными параметрами.
В заключении можно сказать, что реализации множественного наследования ведут к появлению сильных зависимостей в коде, а следовательно такое наследование желательно использовать только с интерфейсами.
Возможной заменой множественного наследования
не от интерфейсов является агрегация:
class CowSniper {
private:
Cow c;
Sniper s;
};