Множественное наследование, дружба
Содержание
Базовые принципы множественного наследования
Расположение классов в памяти при множественном наследовании. От порядка родителей будет зависеть то, как лежат данные.
struct D: A,B,C
{
};
Конструкторы, деструкторы
От порядка родителей зависит порядок вызова конструкторов и деструкторов.
Конструкторы: A->B->C->D
Деструкторы: D->C->B->A
Как использовать множественное наследоание
В основном множественное наследование используется при наследовании от интерфейсов. Интерфейс в C++ это класс, у которого все функции виртуальные. Наследование реализации применяется очень редко.
Пример наследования реализации:
Мы видим что второй вариант с одиночным наследованием проще и логичнее.
Пример злоупотребления:
struct Circle
{
Point center_;
double radius_;
};
Но некоторые делают:
struct Circle: Point
{
double radius_;
};
Как мы видим, множественное наследование порой логичнее и проще заменить на включение объекта внутрь нашего класса. Одним из аргументов против множественного наследования является то, что в большинстве промышленных языков его нет, и там без него обходятся без особых сложностей.
Пример наследования от интерфейсов
Наследование от нескольких интерфейсов является одним из самых оправданных случаев применения множественного наследования.
Другие особенности множественного наследования
Если в базовых классах есть метод с одинаковым именем, и мы его переопределили в наследнике, то он переопределятся сразу для всех родителей.
Способ обойти это ограничение:
Можно явно вызвать виртуальный метод базового класса
struct A
{
virtual void foo()=0;
};
struct B
{
virtual void foo()
{
std::cout << "B::foo()" << std::endl;
}
};
struct C:A,B
{
virtual void foo()
{
std::cout << "C::foo()" << std::endl;
}
};
int main()
{
C c;
c.B::foo();
system("PAUSE");
return 0;
}
Результат: B::foo();
Таблица виртуальных функций и конструкторы
Во время последовательного вызова конструкторов при создании объекта класса, внутри тела каждого конструктора указатель на таблицу виртуальных функций будет установлен на тот класс, для которого вызван конструтор. Так как контекст будет изменяться при вызове конструторов, вызывать в них виртуальные функции является плохим тоном.
Виртуальное наследование
Рассмотрим следующую иерархию
При обычно наследовании мы бы получили в классе C две копии класса A. Но если семантика нашего наследования предполагает что C является A, также как B1 и B2, то мы получаем логическое противоречие такой конструкции. Также такая конструкция будет не верна, если нам нужно иметь одну копию класса A в C. Для преодоления этой ситуации в C++ было введено виртуальное наследование. Ключевое слово virtual при наследовании показывает компилятору, что класс наследник может учавствовать в ромбовидных иерархиях.
Вот как будет выглядеть код этого примера с виртуальным наследованием:
struct A
{
void foo()
{
std::cout << "A" << std::endl;
}
};
struct B1: virtual A
{
};
struct B2: virtual A
{
};
struct C:B1,B2
{
};
int main()
{
C c;
c.foo();
system("PAUSE");
return 0;
}
Как это работает
Компилятор добавляет в таблицу виртуальных функций класса наследника функцию, которая возвращает указатель на объект базового класса. Таким образом эти функции у B1 и B2 будут возвращать один и тот же указатель на объект A, сожержащийся в C. Реализация виртуального наследования не регламентируется, этому есть и другие подходы, например, основанный на отдельной таблице виртуальных классов.
"Дружба"
В С++ существует ключевое слово friend, означающее что класс или функция будут видеть все данные и методы класса, с которым они дружат. Если A друг B, то это не значит что B друг A.
Пример (Функция foo Дружит с классом Bar):
struct Bar
{
friend void foo(Bar& bar);
private:
static int data_;
void MakeCoffee()
{
}
};
void foo(Bar& bar)
{
bar.MakeCoffee();
std::cout << Bar::data_ << std::endl;
}
int Bar::data_;