Повторное использование класса через его расширение
Вы создавали классы с конструкторами и убирали за ними с помощью деструкторов. Наследование - следующий шаг: вместо того чтобы копировать члены одного класса в другой, вы объявляете, что новый класс является специализированной версией существующего, и позволяете ему унаследовать всё автоматически.
Существующий класс - это базовый класс (или родитель); новый - производный класс (или потомок). Производный класс начинает со всеми данными и поведением базового, а затем добавляет или изменяет то, что делает его особенным. Это главный инструмент C++ для отношения «является»: Dog является Animal, SavingsAccount является BankAccount.
Базовый синтаксис: class Derived : public Base
Вы наследуете, ставя после имени производного класса двоеточие, спецификатор доступа и имя базового класса. Самая распространённая форма - public-наследование.
Dog нигде не объявляет ни name, ни eat(), но оба работают с объектом Dog, потому что были унаследованы от Animal. Производный класс волен добавлять члены вроде bark(), о которых базовый класс ничего не знает.
protected: члены только для потомков
private-член базового класса недоступен внутри производного - наследование не нарушает инкапсуляцию. Когда внутреннее устройство базового класса нужно скрыть от внешнего мира, но сделать доступным подклассам, используйте спецификатор доступа protected.
Представьте три уровня как концентрические кольца: private - «только этот класс», protected - «этот класс и его потомки», а public - «все». Полную картину смотрите в спецификаторах доступа.
Порядок конструкторов и деструкторов
Производный объект содержит подобъект базового класса, и эта базовая часть должна быть жива до того, как будет построена производная. Поэтому конструирование идёт начиная с базового, а разрушение - начиная с производного (строго обратный порядок). Если базовому классу нужны аргументы конструктора, вы передаёте их через список инициализации членов.
Вывод делает порядок наглядным:
Animal ctor: Rex // базовый строится первым
Dog ctor // затем производная часть
Dog dtor // разрушается в обратном порядке...
Animal dtor: Rex // ...базовый последним
Если вы забудете : Animal(n), а у базового класса нет конструктора по умолчанию, код не скомпилируется - C++ не знает, как построить базовую часть. Базовый класс, от которого вы собираетесь наследовать, почти всегда должен объявлять деструктор (и, как покажет следующая страница, часто виртуальный).
Переопределение: новое определение метода базового класса
Производный класс может заменить унаследованный метод, объявив метод с той же сигнатурой. Добраться до оригинала по-прежнему можно через Base::method().
Это обычное сокрытие имён (name hiding), а не полиморфизм: какой именно describe() выполнится, решается на этапе компиляции по статическому типу переменной. Это важное ограничение - если вызвать через Shape& или Shape*, который на самом деле указывает на Circle, вы всё равно получите Shape::describe(). Чтобы это исправить, нужен virtual - тема следующей страницы.
Остерегайтесь срезки объектов (slicing)
Поскольку ссылка или указатель на базовый класс могут ссылаться на производный объект, возникает соблазн скопировать производный объект в переменную базового типа. Не делайте этого - производная часть будет срезана.
a - это настоящий Animal, а не Dog под вывеской базового класса, поэтому breed у него попросту не существует. Чтобы работать с производными объектами полиморфно, нужно использовать ссылку на базовый класс (Animal&) или указатель (Animal*), но никогда не значение базового типа. Срезка происходит молча - код компилируется без ошибок и просто тихо теряет данные, - из-за чего это одна из самых легко попадающих в продакшен ошибок наследования.
Типичные ошибки, которых стоит избегать
- Ожидать, что
private-члены базового класса доступны в потомке. Они недоступны. Используйтеprotectedдля данных, которые законно нужны производному классу, а действительно внутреннее состояние держитеprivate. - Забыть передать аргументы конструктора базового класса. Если у базового класса нет конструктора по умолчанию, его нужно вызвать явно в списке инициализации конструктора производного класса (
: Base(args)). - Срезать производный объект в значение базового типа. Копирование
DogвAnimalотбрасывает всё, что специфично дляDog. Вместо этого передавайте и храните ссылки или указатели на базовый класс. - Злоупотреблять наследованием ради повторного использования кода. Наследование моделирует «является». Если отношение на самом деле «имеет» (
CarимеетEngine), предпочтите композицию - объект-член - наследованию.
Далее: виртуальные функции
Переопределение, которое вы только что видели, разрешалось на этапе компиляции, поэтому вызов через указатель на базовый класс игнорировал производную версию. Следующая страница, виртуальные функции, знакомит с ключевым словом virtual и override - механизмом, благодаря которому тип времени выполнения решает, какой метод выполнится; он открывает настоящий полиморфизм и объясняет, почему базовым классам нужны виртуальные деструкторы.
Часто задаваемые вопросы
Что такое наследование в C++?
Наследование позволяет определить новый класс (производный) на основе уже существующего (базового). Производный класс автоматически получает данные-члены и функции-члены базового, а также может добавлять новые или заменять существующее поведение. Оно моделирует отношение «является» (is-a) - Dog является Animal - и служит главным способом, которым C++ повторно использует и расширяет код в иерархиях классов.
В чём разница между public- и private-наследованием в C++?
При public-наследовании (class Dog : public Animal) публичный интерфейс базового класса остаётся публичным и в производном, поэтому Dog является Animal и может использоваться везде, где ожидается Animal. При private-наследовании унаследованные члены становятся приватными - производный класс повторно использует реализацию базового, но не взаимозаменяем с ним. Public-наследование - безусловно, самый частый случай; к private прибегайте только для повторного использования в духе «реализовано-через».
В каком порядке вызываются конструкторы и деструкторы при наследовании в C++?
Конструкторы выполняются начиная с базового: базовый класс полностью конструируется до того, как выполнится тело конструктора производного класса. Деструкторы выполняются в строго обратном порядке - сначала производный, затем базовый. Это гарантирует, что пока производный объект собирается или разрушается, каждая часть, от которой он зависит, уже существует (или ещё существует).