До сих пор мы рассматривали только одиночные наследования, когда дочерний класс имеет только одного родителя. Однако C++ предоставляет возможность множественного наследования.
Множественное наследование
Множественное наследование позволяет одному дочернему классу иметь несколько родителей. Предположим, что мы хотим написать программу для отслеживания работы учителей. Учитель — это Human. Тем не менее, он также является Сотрудником (Employee).
Множественное наследование может быть использовано для создания класса Teacher, который будет наследовать свойства как Human, так и Employee. Для использования множественного наследования нужно просто указать через запятую тип наследования и второй родительский класс:
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 |
#include <string> class Human { private: std::string m_name; int m_age; public: Human(std::string name, int age) : m_name(name), m_age(age) { } std::string getName() { return m_name; } int getAge() { return m_age; } }; class Employee { private: std::string m_employer; double m_wage; public: Employee(std::string employer, double wage) : m_employer(employer), m_wage(wage) { } std::string getEmployer() { return m_employer; } double getWage() { return m_wage; } }; // Класс Teacher открыто наследует свойства классов Human и Employee class Teacher: public Human, public Employee { private: int m_teachesGrade; public: Teacher(std::string name, int age, std::string employer, double wage, int teachesGrade) : Human(name, age), Employee(employer, wage), m_teachesGrade(teachesGrade) { } }; |
Здесь мы используем наследование типа public.
Проблемы с множественным наследованием
Хотя множественное наследование кажется простым расширением одиночного наследования, оно может привести к множеству проблем, которые могут заметно увеличить сложность программ и сделать кошмаром дальнейшую поддержку кода. Рассмотрим некоторые из подобных ситуаций.
Во-первых, может возникнуть неоднозначность, когда несколько родительских классов имеют метод с одним и тем же именем, например:
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 |
#include <iostream> class USBDevice { private: long m_id; public: USBDevice(long id) : m_id(id) { } long getID() { return m_id; } }; class NetworkDevice { private: long m_id; public: NetworkDevice(long id) : m_id(id) { } long getID() { return m_id; } }; class WirelessAdapter: public USBDevice, public NetworkDevice { public: WirelessAdapter(long usbId, long networkId) : USBDevice(usbId), NetworkDevice(networkId) { } }; int main() { WirelessAdapter c54G(6334, 292651); std::cout << c54G.getID(); // какую версию getID() здесь следует вызывать? return 0; } |
При компиляции c54G.getID()
компилятор смотрит, есть ли у WirelessAdapter метод getID(). Этого метода у него нет, поэтому компилятор двигается по цепочке наследования вверх и смотрит, есть ли этот метод в каком-либо из родительских классов. И здесь возникает проблема — getID() есть как у USBDevice, так и у NetworkDevice. Следовательно, вызов этого метода приведет к неоднозначности и мы получим ошибку, так как компилятор не будет знать какую версию getID() ему вызывать.
Тем не менее, есть способ обойти эту проблему. Мы можем явно указать, какую версию getID() следует вызывать:
1 2 3 4 5 6 7 |
int main() { WirelessAdapter c54G(6334, 292651); std::cout << c54G.USBDevice::getID(); return 0; } |
Хотя это решение довольно простое, но всё может стать намного сложнее, если наш класс будет иметь от 4 родительских классов, которые, в свою очередь, будут иметь свои родительские классы. Возможность возникновения конфликтов имен увеличивается экспоненциально с каждым добавленным родительским классом, и в каждом из таких случаев нужно будет явно указывать версии методов, которые следует вызывать, дабы избежать возможности возникновения конфликтов имен.
Во-вторых, более серьезной проблемой является «алмаз смерти» (или «алмаз обреченности»). Это ситуация, когда один класс имеет 2 родительских класса, каждый из которых, в свою очередь, наследует свойства одного и того же родительского класса. Иллюстративно мы получаем форму алмаза.
Например, рассмотрим следующие классы:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
class PoweredDevice { }; class Scanner: public PoweredDevice { }; class Printer: public PoweredDevice { }; class Copier: public Scanner, public Printer { }; |
Сканеры и принтеры — это устройства, которые получают питание от розетки, поэтому они наследуют свойства PoweredDevice. Однако ксерокс (Copier) включает в себя функции как сканеров, так и принтеров.
В этом контексте возникает много проблем, включая неоднозначность при вызове методов и копирование данных PoweredDevice в класс Copier дважды. Хотя большинство из этих проблем можно решить с помощью явного указания, поддержка и обслуживание такого кода может привести к непредсказуемым временным затратам. Мы поговорим детально о способах решения проблемы «алмаза смерти» на соответствующем уроке.
Стоит ли использовать множественное наследование?
Большинство задач, решаемых с помощью множественного наследования, можно решить и с использованием одиночного наследования. Многие объектно-ориентированные языки программирования (например, Smalltalk, PHP) даже не поддерживают множественное наследование. Многие, относительно современные языки, такие как Java и C#, ограничивают классы одиночным наследованием обычных классов, но допускают множественное наследование интерфейсных классов. Суть идеи, запрещающей множественное наследование в этих языках, заключается в том, что это излишняя сложность, которая порождает больше проблем, чем удобств.
Многие опытные программисты считают, что множественное наследование в языке C++ следует избегать любой ценой из-за потенциальных проблем, которые могут возникнуть. Однако все же остается вероятность, когда множественное наследование будет лучшим решением, нежели придумывание двухуровневых «костылей».
Стоит отметить, что вы сами уже использовали классы, написанные с использованием множественного наследования, даже не подозревая об этом: такие объекты, как std::cin и std::cout библиотеки iostream, реализованы с использованием множественного наследования!
Правило: Используйте множественное наследование только в крайних случаях, когда задачу нельзя решить одиночным наследованием, либо другим альтернативным способом (без изобретения «велосипеда»).
В Python есть понятие MRO (Method Resolution Order) это поиск (разрешение) метода в цепочке наследования. Там играет роль порядок наследования класса. Если метод найден в первом родительском классе, то он и выполнится, дальнейшего поиска не будет. Тем самым, там решен неоднозначности при совпадении имен методов родительских классов.
Здравствуйте! Спасибо Вам огромное, Юрий, за проделанный труд по переводу туториала. Очень удобный и полезный ресурс! Хорошую мысль Вы реализовали — обучать других и самого себя) Хотел спросить : Вы собираетесь перевести все уроки англоязычного курса?
Привет, надеюсь, что да — переведу до конца.