Множественное наследование — это одна из ключевых особенностей языка C++. Рассмотрим, когда оно может потребоваться и как его использовать.
Назначение множественного наследования
Предположим, что нам нужно нарисовать на форме логотип, который состоит из квадрата и круга. Сначала посмотрим, как это сделать на Си. Предположим, что у нас уже есть функции рисования квадрата и круга.
1 2 3 4 5 |
void Logo() { Rectangle(); Circle(); } |
Далее везде, где нужно рисование логотипа, вызываем функцию Logo.
Теперь нам нужно то же самое сделать в C++. Предположим, что у нас уже есть классы рисования квадрата и круга. Как нарисовать логотип?
Можно, конечно, сделать так:
1 2 3 4 5 |
Rectangle Logo1; Circle Logo2; Logo1.Draw(); Logo2.Draw(); |
А потом вызывать везде эту пару строк, но это процедурный подход и он в C++ не приветствуется. В тех случаях, когда нам нужно получить методы разных классов, как раз и используется множественное наследование.
Использование множественного наследования
Для поддержки множественного наследования нужно при определении класса перечислить через запятую базовые классы. В примере для простоты вместо рисования фигур методы Draw просто выводят их названия.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
#include <iostream> using namespace std; // Базовый класс class Rectangle { public: void Draw() { cout << "Rectangle "; } protected: int width; int height; }; // Базовый класс class Circle { public: void Draw() { cout << "Circle "; } protected: int x; int y; int radius; }; // Производный класс class Logo: Rectangle, Circle { public: void DrawLogo() { Rectangle::Draw(); Circle::Draw(); } }; int main(void) { Logo logo; logo.DrawLogo(); return 0; } |
Отличия множественного наследования
В случае единичного наследования производный класс получает доступ к публичным и защищенным элементам базового класса.
В случае множественного наследования производный класс получает доступ к публичным и защищенным элементам всех базовых классов.
Кроме того, в момент создания экземпляра производного класса выполняться все конструкторы базовых классов, а при удалении объекта выполнятся все деструкторы базовых классов.
Ошибка неоднозначности
Если в базовых классах элементы называются одинаково, то в производном классе появляется ошибка неоднозначности. В примере выше, если написать просто
1 |
Draw(); |
то компилятор выдаст ошибку, так непонятно метод какого класса нужно использовать. Ведь метод Draw есть в обоих классах.
Алмаз смерти
Теперь рассмотрим ситуацию, когда один производный класс имеет два базовых класса, при этом каждый из которых является производным одного и того же суперкласса. При этом в производных классах есть свойства с одинаковым именем.
Подобная ситуация получила название «алмаз смерти», так как на диаграмме классов такая ситуация выглядит как алмаз.
В рассмотренном примере добавим суперкласс Shape.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
#include <iostream> using namespace std; // Суперкласс class Shape { public: void Draw() { cout << "Shape "; } protected: int x; int y; }; // Базовый класс class Rectangle :Shape { public: void Draw() { cout << "Rectangle "; } protected: int width; int height; }; // Базовый класс class Circle :Shape { public: void Draw() { cout << "Circle "; } protected: int x; int y; int radius; }; // Производный класс class Logo: Rectangle, Circle { public: void DrawLogo() { Rectangle::Draw(); Circle::Draw(); } }; int main(void) { Logo logo; logo.DrawLogo(); return 0; } |
Мы сразу получаем множество проблем неоднозначности вызова. Так, например, если мы видим в классе Logo координаты (x, y) , то какие именно координаты мы имеем в виду?
Проблемы множественного наследования в крупных проектах
В конце концов если классы пишет один человек, то он сам все-таки представляет, что делать в случае неоднозначности вызова. Но если это используется в группе, то ситуация резко усложняется.
Лучше всего эту картину иллюстрирует карточный домик.
Представьте себе, что каждая карта — это класс, который разрабатывает отдельный программист. Нижние карты — это базовые классы, а все остальные — производные классы. И теперь представьте, что вам достался самый верхний этаж.
Вы написали класс, запустили программу и получили «алмаз смерти». К какому программисту вы будете обращаться?
Запрет наследования
Посмотрим на проблему и с другой стороны. Представьте, что вы год назад написали класс, который находится в середине иерархии классов. Отладили его, провели тесты и с ним все в порядке. Сейчас вы пишете другой класс, при этом у вас, как обычно, что-то не получается и сроки поджимают.
В этот момент к вам подходит коллега и говорит:
— Слушай, я унаследовал твой класс и класс Угрюмова. И получил алмаз смерти. Иди и решай с Угрюмовым проблему неоднозначности.
Как вы понимаете ответить этому программисту хочется только одно:
— А может ты не будешь наследовать мой класс?!
Для подобных случаев в стандарт C++ 11 добавлен спецификатор final, который запрещает наследовать данный класс. Для этого нужно написать ключевое слово final после имени класса.
1 |
class Logo final: Rectangle, Circle |
После этого появление «алмаза смерти» в этой ветке наследования будет исключено, так не будет самого наследования. Компилятор не даст создать производный класс от данного класса.
Стоит ли использовать множественное наследование?
Если вы пишете программу один, то «алмаз смерти» для вас не страшен. Вы легко устраните неоднозначность. Но для группы программистов это может стать большой проблемой.
Кроме того, в любой программе есть ошибки. Это реальность программирования. Получается, что производный класс собирает ошибки всех базовых классов.
В большом проекте подобная ситуация может привести к тому, время время на поиски ошибки может превысить все разумные сроки.
Все это привело к тому, что в языках программирования Objective-C, Java и C#, которые были предложены на смену C++, от множественного наследования отказались.