Наследование — механизм ООП, при котором новый класс получает поля и методы существующего класса и может дополнить или переопределить их поведение. Одно и то же отношение в двух языках называют по-разному: в C++ говорят базовый класс (base class) и производный класс (derived class), а суперкласс (superclass) и подкласс (subclass) — термины Java. Если вы ищете «суперкласс в C++», правильное слово там — базовый класс; синоним «родительский класс» понятен в обоих языках.
Содержание
- Термины: что как называется
- Один пример на C++ и Java
- super и вызов конструктора базового класса
- virtual и override: главное различие
- Виды наследования: public, protected, private
- Множественное наследование и ромб
- Приведение типов: upcast и downcast
- Сводная таблица различий
- Выводы
- Где применяется / связь с практикой
- FAQ
Ниже — один и тот же пример на двух языках, вызов конструктора и методов базового класса, virtual и @Override, множественное наследование и сводная таблица различий. Код C++ проверен в Apple clang 21 (-std=c++20), код Java — в OpenJDK 25.0.4 (Java 25 LTS), 24 сентября 2026 года.
Термины: что как называется
| Понятие | C++ | Java |
|---|---|---|
| Класс, от которого наследуют | базовый класс | суперкласс |
| Класс-наследник | производный класс | подкласс |
| Синтаксис | class Laptop : public Device |
class Laptop extends Device |
| Вызвать конструктор родителя | список инициализации : Device(name) |
super(name); |
| Вызвать метод родителя | Device::describe() |
super.describe() |
Ключевого слова super в C++ нет: к члену базового класса обращаются по его имени через ::. Обычно это объясняют множественным наследованием: при двух базовых классах слово super было бы неоднозначным. У MSVC есть нестандартное расширение __super, но в переносимом коде пишут имя базового класса.
Один пример на C++ и Java
Задача: есть класс Device с именем и методом describe(). Класс Laptop наследует его, добавляет заряд батареи и дополняет описание.
Версия на C++
#include <iostream>
#include <memory>
#include <string>
class Device { // базовый класс
public:
explicit Device(std::string name) : name_(std::move(name)) {
std::cout << "Device(" << name_ << ")\n";
}
virtual ~Device() { std::cout << "~Device\n"; }
virtual std::string describe() const { return "device " + name_; }
protected:
std::string name_;
};
class Laptop : public Device { // производный класс
public:
Laptop(std::string name, int battery)
: Device(std::move(name)), battery_(battery) { // вызов конструктора базового
std::cout << "Laptop\n";
}
~Laptop() override { std::cout << "~Laptop\n"; }
std::string describe() const override {
return Device::describe() + ", battery " + std::to_string(battery_) + "%";
}
private:
int battery_;
};
int main() {
std::unique_ptr<Device> d = std::make_unique<Laptop>("X1", 80);
std::cout << d->describe() << "\n";
}
Вывод программы:
Device(X1)
Laptop
device X1, battery 80%
~Laptop
~Device
Что здесь видно. Конструкторы вызываются от базового к производному, деструкторы — в обратном порядке. Хотя переменная d имеет тип указателя на Device, вызывается Laptop::describe(): метод объявлен virtual. Внутри него Device::describe() вызывает версию базового класса.
Версия на Java
class Device { // суперкласс
protected final String name;
Device(String name) {
this.name = name;
System.out.println("Device(" + name + ")");
}
String describe() { return "device " + name; }
}
class Laptop extends Device { // подкласс
private final int battery;
Laptop(String name, int battery) {
super(name); // вызов конструктора суперкласса
this.battery = battery;
System.out.println("Laptop");
}
@Override
String describe() {
return super.describe() + ", battery " + battery + "%";
}
}
public class Main {
public static void main(String[] args) {
Device d = new Laptop("X1", 80); // upcast
System.out.println(d.describe());
if (d instanceof Laptop l) { // безопасный downcast
System.out.println("it is a laptop: " + l.name);
}
}
}
Вывод:
Device(X1)
Laptop
device X1, battery 80%
it is a laptop: X1
Деструкторов в Java нет: память освобождает сборщик мусора, а для закрытия ресурсов используют try-with-resources. Слово virtual тоже не нужно — в Java переопределяемые методы виртуальные по умолчанию.
super и вызов конструктора базового класса
Конструкторы не наследуются ни в C++, ни в Java: каждый класс объявляет свои и из них вызывает конструктор родителя.
- C++. Конструктор базового класса вызывается в списке инициализации:
Laptop(...) : Device(name). Если его не указать, вызовется конструктор базового класса без аргументов, а если такого нет — ошибка компиляции. С C++11 можно унаследовать конструкторы строкойusing Device::Device;. - Java. Вызов
super(name)пишут в конструкторе подкласса. Если его нет, компилятор подставитsuper()без аргументов, и при отсутствии такого конструктора в суперклассе код не соберется.
В Java до версии 24 включительно super(...) обязан был стоять первой инструкцией. В Java 25 стали стандартными «гибкие тела конструкторов» (JEP 513): до super(...) можно проверить аргументы и присвоить значения собственным полям подкласса, у которых нет инициализатора, но нельзя читать поля создаваемого объекта, вызывать его методы и передавать this.
Laptop(String name) {
if (name.isBlank()) throw new IllegalArgumentException("empty name");
super(name);
}
На Java 25 это компилируется, а с javac --release 21 выдает ошибку:
Main.java:7: error: flexible constructors is not supported in -source 21
super(name);
^
(use -source 25 or higher to enable flexible constructors)
virtual и override: главное различие
В Java вызов обычного метода экземпляра выбирается по реальному типу объекта. Исключения — static, private и final методы: их нельзя переопределить. В C++ наоборот: без virtual метод выбирается по типу переменной (указателя или ссылки), а не объекта.
Неверный код — в C++ забыли virtual:
#include <iostream>
struct Device {
const char* describe() const { return "device"; } // нет virtual
};
struct Laptop : Device {
const char* describe() const { return "laptop"; }
};
int main() {
Laptop l;
Device& d = l; // ссылка на базовый класс
std::cout << l.describe() << " / " << d.describe() << "\n";
}
Результат — через ссылку на базовый класс вызывается его версия:
laptop / device
Исправление — virtual const char* describe() const в Device, тогда программа печатает laptop / laptop. По той же причине базовый класс, объекты которого удаляют через указатель на базовый тип, должен иметь virtual-деструктор: без него delete через такой указатель — неопределенное поведение, и деструктор производного класса может не вызваться.
Вторая типичная ошибка — сигнатура «почти совпала». Спецификатор override (C++11) и аннотация @Override в Java просят компилятор проверить, что метод действительно переопределяет родительский. В C++ забыли const:
struct Laptop : Device {
const char* describe() override { return "laptop"; } // забыли const
};
clang 21 выдает:
error: non-virtual member function marked 'override' hides virtual member function
В Java опечатка в имени метода с @Override дает error: method does not override or implement a method from a supertype. Без этих пометок оба компилятора молча создали бы новый метод. Точный текст ошибки зависит от компилятора: GCC и MSVC формулируют ее иначе.
Виды наследования: public, protected, private
В C++ режим наследования меняет доступ к унаследованным членам снаружи:
public— публичное остается публичным, защищенное — защищенным. Это обычный выбор для отношения «является» (ноутбук является устройством).protected— публичные члены базового класса становятся защищенными.private— все унаследованное становится закрытым; снаружиLaptopнельзя вызвать методDevice, иLaptopнельзя передать туда, где ждутDevice&.
Режим по умолчанию зависит от ключевого слова: у class — private, у struct — public. Поэтому class Laptop : Device — это закрытое наследование, частая неожиданная ошибка.
В Java режима наследования нет: extends всегда сохраняет доступ как есть. private члены суперкласса подклассу недоступны, protected доступны подклассу и всему пакету. Запретить наследование можно в обоих языках: final class в Java и class Laptop final в C++.
Множественное наследование и ромб
В C++ класс может иметь несколько базовых классов. Проблема — «ромб»: если Computer и Monitor оба наследуют Device, а Laptop наследует оба, в Laptop окажутся две копии Device.
#include <iostream>
struct Device {
Device() { std::cout << "Device()\n"; }
int serial = 42;
};
struct Computer : Device {};
struct Monitor : Device {};
struct Laptop : Computer, Monitor {};
int main() {
Laptop l;
std::cout << l.serial << "\n";
}
Обращение к l.serial не компилируется: clang сообщает, что serial найден в нескольких подобъектах Device. Исправление — виртуальное наследование: struct Computer : virtual Device и struct Monitor : virtual Device. Тогда Device в объекте один, его конструктор вызывается один раз, и программа печатает Device() и 42.
В Java класс расширяет только один класс, но может реализовать несколько интерфейсов. Конфликт двух default-методов с одинаковой сигнатурой компилятор не пропустит (types Charger and Display are incompatible), пока класс не переопределит метод сам:
interface Charger { default String info() { return "charger"; } }
interface Display { default String info() { return "display"; } }
class Laptop implements Charger, Display {
@Override
public String info() {
return Charger.super.info() + " + " + Display.super.info();
}
}
public class Main {
public static void main(String[] args) {
System.out.println(new Laptop().info()); // charger + display
}
}
Приведение типов: upcast и downcast
Upcast (ссылка на подкласс как на суперкласс) безопасен в обоих языках и делается неявно. Downcast требует проверки:
- в Java —
instanceofс шаблоном, как в примере выше; прямое(Laptop) dдля объекта другого типа бросаетClassCastException: class Phone cannot be cast to class Laptop; - в C++ —
dynamic_cast<Laptop*>(ptr)возвращаетnullptr, если объект неLaptop(нужен хотя бы один виртуальный метод в базовом классе);static_castтакую проверку не делает.
Сводная таблица различий
| Что сравниваем | C++ | Java |
|---|---|---|
| Классов-родителей | несколько | один класс + любое число интерфейсов |
| Виртуальность методов | только с virtual |
по умолчанию (кроме static, private, final) |
| Проверка переопределения | override |
@Override |
| Обращение к родителю | Base::method() |
super.method() |
| Режимы наследования | public, protected, private |
нет, доступ сохраняется |
| Деструкторы | есть, вызов в обратном порядке | нет, сборщик мусора |
| Запрет наследования | final после имени класса |
final class |
| Проверяемый downcast | dynamic_cast |
instanceof |
Когда наследование не нужно. Если отношение не «является», а «имеет» (у ноутбука есть батарея), вместо наследования используют композицию — поле нужного типа. Глубокие иерархии хрупки: правка базового класса затрагивает всех наследников. Как наследование работает вместе с полиморфизмом в объектной разработке в целом, разобрано в статье о полиморфизме и ООП.
Выводы
- В C++ говорят «базовый» и «производный» класс, в Java — «суперкласс» и «подкласс»; речь об одном отношении.
- Конструктор родителя вызывается списком инициализации в C++ и
super(...)в Java; конструкторы не наследуются (кромеusing Base::Baseв C++). - В Java методы виртуальные по умолчанию, в C++ нужен
virtual, а базовому классу с полиморфным удалением — виртуальный деструктор. overrideи@Overrideпревращают опечатку в сигнатуре в ошибку компиляции.- C++ допускает множественное наследование и решает ромб через
virtual-наследование; Java ограничивает классы одним родителем и разрешает конфликты интерфейсов явным переопределением.
Где применяется / связь с практикой
Освойте тему на практике
Иерархии классов встречаются в любом Java-проекте: исключения, коллекции, классы фреймворков, от которых наследуют свои компоненты. Понимание super, переопределения и приведения типов нужно, чтобы читать такой код и не ломать контракт родителя. Эти темы с практикой и ревью кода разбирают на курсе «Java разработчик. Базовый уровень». Попробовать формат можно на бесплатных открытых уроках.
FAQ
Наследуются ли private-поля?
Физически они есть в объекте наследника в обоих языках, но обратиться к ним по имени из подкласса нельзя — только через методы родителя.
Можно ли в C++ вызвать виртуальный метод из конструктора и получить версию наследника?
Нет. Во время работы конструктора Device объект еще считается Device, поэтому вызовется версия базового класса. В Java вызовется версия подкласса, но его поля еще не инициализированы — это источник трудноуловимых ошибок.
Чем интерфейс в Java отличается от абстрактного класса C++ с чисто виртуальными методами?
Интерфейс не хранит состояние экземпляра и допускает множественную реализацию; абстрактный класс C++ может иметь поля и конструкторы. Ближайший аналог интерфейса в C++ — класс только из чисто виртуальных методов.



