Наследование в программировании на примере C++ и Java

Наследование в программировании на примере C++ и Java Полезное

Наследование — механизм ООП, при котором новый класс получает поля и методы существующего класса и может дополнить или переопределить их поведение. Одно и то же отношение в двух языках называют по-разному: в C++ говорят базовый класс (base class) и производный класс (derived class), а суперкласс (superclass) и подкласс (subclass) — термины Java. Если вы ищете «суперкласс в C++», правильное слово там — базовый класс; синоним «родительский класс» понятен в обоих языках.

Ниже — один и тот же пример на двух языках, вызов конструктора и методов базового класса, 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++ — класс только из чисто виртуальных методов.

OTUS Журнал
Скидка 5% 14-20 сентября на курсы (popup)