Наследование – это один из механизмов объектно-ориентированного программирования. Важно заметить, что в наше время практически каждый современный язык поддерживает эту парадигму. Но стоит сказать, что в C++ есть некоторые особенности в возможностях этого механизма.
Обычно под наследованием понимается отношение «является». Т.е. когда вы пишете (на C#):
public class B: A { /*…*/ }
или (на C++)
class B: public A { /*…*/ };
то вы говорите, что класс B является наследником A. Это также означает, что любой объект типа B также является объектом (разновидностью) типа A (но не наоборот!).
При помощи этого механизма строится иерархия классов. Но стоит с осторожностью оперировать им. Например, вы создали класс TextBox. В довесок был также создан класс TextBoxInfo, содержащий различные вспомогательные методы, которые по какой-то причине оказываются очень полезны основному классу TextBox. Ну и отлично: унаследуем класс TextBoxInfo для получения доступа к реализации этих самых методов:
class TextBoxInfo { public: TextBoxInfo(){ /*…*/ } virtual ~TextBoxInfo(){ /*…*/ } virtual void doSmthWithText(){ /*…*/} // ... }; class TextBox: public TextBoxInfo { /*…*/ };
Все вроде бы так, но не совсем. Это неверно, по крайней мере, с логической точки зрения. Попробуем разобраться. Открытым наследованием мы, по сути, добавляем нового участника «семьи» с какими-то общими данными. Однако в нашем коде мы явно указали, что TextBox – это разновидность некой сущности TextBoxInfo, что в корне неверно. TextBoxInfo – это вспомогательный класс. Даже если он содержит необходимый функционал, то это еще не повод наследовать его.
Абзацем выше я упомянул слово «открытый», когда говорил о наследовании. Разве оно бывает каким-то иным? В C++, по крайней мере, да, бывает. А именно:
- Открытое (public) наследование. Как уже писалось, отражает зависимость «являться». Все члены базового класса становятся членами класса-наследника (как интерфейс, так и реализация).
- Закрытое (private) наследование. Об этом подробнее.
- Защищенное (protected) наследование – очень редко встречающийся на практике вид наследования. При protected-наследовании все члены с модификатором public наследуются с модификатором protected (т.е. все открытые члены базового класса становятся защищенными членами класса-наследника).
Пусть имеется класс B, который открыто наследует класс A. Такое наследование дает ряд преимуществ. Одним из них является то, что можно обращаться к классам-наследникам через указатель на базовый класс, т.е.:
A *some = new B; // все хорошо: A – базовый класс, произойдет неявное преобразование
Но теперь проделаем небольшую корректировку (а именно заменим public-наследование на private):
class A { /*…*/ }; class B: private A { /*…*/ };
Если попытаться написать
A *some = new B;
То компилятор выдаст ошибку. Поясню почему: public-наследованием вы явно говорите, что объект одного типа _ является_ разновидностью другого типа, а по поводу private такого сказать нельзя. Private-наследование используется только на этапе реализации, и оно скорее выражает зависимость «реализовано на основе». Т.е. на этапе проектирования private-наследование никоим образом не фигурирует, а появляется только на этапе написания программного кода.
Вернемся к нашему примеру с классом TextBox. Мы не можем открыто унаследовать TextBoxInfo по логическим и идеологическим соображениям. Но мы можем написать:
class TextBoxInfo { public: TextBoxInfo(){ /*…*/ } virtual ~TextBoxInfo(){ /*…*/ } virtual void doSmthWithText(){ /*…*/ } // … }; class TextBox: private TextBoxInfo { /*…*/ };
Таким образом, мы говорим, что класс TextBox _реализован при помощи_ класса TextBoxInfo, но при этом он не является разновидностью этого класса.
Стоит отметить, что private-наследование в большинстве случаев можно заменить обычным внедрением, т.е. созданием члена-указателя на нужный класс.
Все, что было описано выше, относится к одиночному наследованию. Однако некоторые языки также поддерживают множественное наследование. C++ в этом плане более свободный, потому что он позволяет сделать n-ное количество наследований у любого класса или структуры (в то время как, например, в Java или C# множественно наследовать можно только интерфейсы).
С одной стороны множественное наследование позволяет с большим простором строить иерархии классов, но с другой есть ряд опасных моментов. Традиционный (который описывается во всех книжках по ООП):
class A { public: void doSmth(){ /*…*/ } // … }; class B { public: void doSmth(){ /*…*/ } void doSmthElse(){ /*…*/ } // … }; class C: public A, public B { /*…*/ };
Как вы уже догадались проблема в совпадающих именах методов. При вызове C::doSmth() появится неопределенное поведение, т.к., по идее, не понятно, какой экземпляр метода вызвать: из класса A или класса B. Одно из решений – это явно указать, что вы хотите использовать именно какую-то конкретную реализацию метода:
C some; some.B::doSmth();
Приемлемое решение проблемы, но только в том случае, когда doSmth объявлена как public в интересующем нас классе.
Другое решение проблемы – использовать виртуальное (virtual) наследование. Но стоит отметить, что сам механизм полиморфизма всегда снижает скорость работы программы и количество съедаемой памяти. Виртуальное наследование – не исключение. Стоит несколько раз подумать, прежде чем его использовать.
Проблема совпадения имен при наследовании – это грубейшая упущение, которое происходит еще на этапе проектирования. При тщательном и грамотном проектировании таких оплошностей, как правило, возникать не должно.
Приведем один из примеров «полезного» применения множественного наследования. Вернемся к нашему классу TextBox. Предположим, что существует класс (интерфейс) ITextArea, который является родителем для всех основных визуальных компонентов, использующих текстовые поля.
class ITextArea { public: virtual void setText( std::string str ) = 0; virtual int getTextSize() = 0; // … }; class TextBoxInfo{ /*…*/ }; // различные функции, помогающие в реализации интерфейса class TextBox: public ITextArea, private TextBoxInfo{ /*…*/ };
В этом примере мы добились довольно элегантного результата: объединили открытое наследование _интерфейса_ с закрытым наследованием _реализации_. Данный пример показывает, что множественное наследование не такое вселенское зло, каким его многие считают. При нужном подходе можно сделать всю иерархию, основанную даже на множественном наследовании более понятной и логичной.
Напоследок хотелось бы подчеркнуть важность механизма наследования в рамках парадигмы ООП. Конечно, можно стараться писать проекты без использования наследования. Но при таком раскладе дел можно только усложнить разработку и ухудшить читаемость кода. А наследование приучает к так называемому «повторному использованию» кода и в некотором смысле ликвидирует его дублирование.
я ни**я не понел!!1
Поясните, что конкретно вам непонятно. Будем разбираться
.
Спасибо за блог, очень интересный подход, подписался на ваш блог, буду заходить чаще.
концероген, неудивительно
Здравствуйте!
Как разрешить неоднозначность вызова одноименных функции в производном классе при помощи квалификатора понятно.
А вот как для разрешения противоречия при множественном наследовании применить виртуальные функции — не понял. Можете привести пример?
class A
{
int a;
public:
void foo(){ a = 0;};
};
class B1 : virtual public A
{
int b1;
};
class B2 : virtual public A
{
int b2;
};
class C : public B1, public B2
{
int c;
};
int _tmain(int argc, _TCHAR* argv[])
{
С c;
c.foo();
return 0;
}
Без виртуального наследования вызов c.foo() привел бы к неопределенному поведению, а скорее всего вызвал бы ошибку на этапе компиляции. С виртуальным же наследованием компилятор проведет все необходимые операции по приведению типов и код успешно исполнится.
Похожая иерархия (называется «ромбовидной») применяется в стандартной библиотеке C++, только вместо A, B1, B2 и C там участвуют шаблонные классы basic_ios, basic_istream, basic_ostream и basic_iostream.