Menu

Наследование в C++: базовые и производные классы

Узнайте, как наследование в C++ позволяет производному классу повторно использовать и расширять базовый класс: синтаксис, public- и private-наследование, порядок вызова конструкторов и деструкторов, а также ловушки вроде срезки объектов (slicing).

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Повторное использование класса через его расширение

Вы создавали классы с конструкторами и убирали за ними с помощью деструкторов. Наследование - следующий шаг: вместо того чтобы копировать члены одного класса в другой, вы объявляете, что новый класс является специализированной версией существующего, и позволяете ему унаследовать всё автоматически.

Существующий класс - это базовый класс (или родитель); новый - производный класс (или потомок). Производный класс начинает со всеми данными и поведением базового, а затем добавляет или изменяет то, что делает его особенным. Это главный инструмент 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++?

Конструкторы выполняются начиная с базового: базовый класс полностью конструируется до того, как выполнится тело конструктора производного класса. Деструкторы выполняются в строго обратном порядке - сначала производный, затем базовый. Это гарантирует, что пока производный объект собирается или разрушается, каждая часть, от которой он зависит, уже существует (или ещё существует).

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ