Интерфейсы в C++: Как объявить и не сломать себе мозг

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Интерфейсы в C++: Гайд для тех, кто устал гуглить

Если вы когда-нибудь писали на Java или C#, а потом перешли на C++, то наверняка задавались вопросом: "А где тут ключевое слово interface?". Спойлер: его нет. В C++ интерфейсы реализуются иначе — через абстрактные классы. И это не баг, а фича, которая даёт невероятную гибкость.

На Stack Overflow вопрос "How do you declare an interface in C++?" собирает тысячи просмотров. Это значит, что даже опытные разработчики иногда путаются. Сегодня мы раз и навсегда разберёмся, как объявить интерфейс в C++, чтобы ваш код был чистым, а компилятор — довольным.

Что такое интерфейс в C++? (Спойлер: это абстрактный класс)

В классическом ООП интерфейс — это контракт. Класс, который реализует интерфейс, обязан предоставить реализацию всех его методов. В C++ эту роль выполняет абстрактный класс — класс, содержащий хотя бы одну чисто виртуальную функцию (pure virtual function).

Запомните главное правило:

  • Интерфейс = класс, где все методы — чисто виртуальные.
  • Абстрактный класс = класс, где хотя бы один метод — чисто виртуальный (но могут быть и обычные методы с реализацией).

Для чистого интерфейса мы используем именно первый вариант: никакой логики, только объявления.

Как объявить чистый интерфейс: базовый синтаксис

Вот минимальный пример интерфейса для геометрической фигуры:

class IShape {
public:
    virtual double area() const = 0;      // Чисто виртуальная функция
    virtual double perimeter() const = 0; // Ещё одна
    virtual ~IShape() {}                  // Виртуальный деструктор — обязателен!
};

Обратите внимание на = 0 в конце объявления. Это и есть магическая пометка "чисто виртуальный". Такой метод не имеет тела (реализации) в базовом классе.

Ключевые моменты:

  • Префикс I в имени класса — негласное соглашение для интерфейсов (I = Interface). Можно не использовать, но это упрощает чтение кода.
  • Виртуальный деструктор обязателен. Если вы удаляете объект через указатель на интерфейс, без виртуального деструктора поведение будет неопределённым (читай: программа упадёт).
  • Все методы public — интерфейс должен быть доступен для вызова извне.

Реализация интерфейса: от слов к делу

Теперь напишем класс, который реализует наш интерфейс IShape. Пусть это будет круг:

class Circle : public IShape {
private:
    double radius;
public:
    explicit Circle(double r) : radius(r) {}

    double area() const override {
        return 3.14159 * radius * radius;
    }

    double perimeter() const override {
        return 2 * 3.14159 * radius;
    }
};

Ключевое слово override (C++11 и новее) не обязательно, но крайне желательно. Оно заставляет компилятор проверить, что вы действительно переопределяете виртуальный метод из базового класса. Опечатались в названии? Компилятор скажет: "Ошибка! Такого метода в интерфейсе нет".

Полиморфизм в действии

Теперь самое интересное — мы можем работать с разными фигурами через единый интерфейс:

void printShapeInfo(const IShape& shape) {
    std::cout << "Area: " << shape.area() << "\n";
    std::cout << "Perimeter: " << shape.perimeter() << "\n";
}

int main() {
    Circle c(5.0);
    printShapeInfo(c); // Работает!
    return 0;
}

Функция printShapeInfo принимает ссылку на IShape. Ей всё равно, какой именно объект передали — круг, квадрат или треугольник. Пока они реализуют интерфейс, всё будет работать. Это и есть полиморфизм в действии.

Частые ошибки новичков (и как их избежать)

Ошибка 1: Забыли виртуальный деструктор

Классика. Если вы пишете:

class IShape {
public:
    virtual double area() const = 0;
    // Деструктор не объявлен — компилятор сгенерирует обычный!
};

...то при удалении объекта через указатель на интерфейс вызовется деструктор базового класса, а деструктор Circle — нет. Утечка памяти гарантирована. Всегда добавляйте virtual ~IShape() {} в интерфейс.

Ошибка 2: Не используете override

Представьте, что вы переименовали метод в интерфейсе с area() на getArea(), но забыли обновить реализацию. Без override компилятор промолчит, а программа будет вызывать пустую базовую версию (и упадёт). С override — получите ошибку компиляции. Всегда используйте override.

Ошибка 3: Пытаетесь создать объект интерфейса

Нельзя написать:

IShape shape; // Ошибка компиляции!

Абстрактный класс нельзя инстанциировать. Это логично: у него нет реализации методов. Используйте указатели или ссылки на интерфейс.

Когда интерфейс — это не абстрактный класс?

Бывает, что вам нужен интерфейс, но без полиморфизма (например, для производительности). В современном C++ (C++20) появились концепты (concepts). Они позволяют проверять, что тип поддерживает определённые методы, на этапе компиляции, без наследования.

template <typename T>
concept Shape = requires(T t) {
    { t.area() } -> std::convertible_to<double>;
    { t.perimeter() } -> std::convertible_to<double>;
};

template <Shape T>
void printShapeInfo(const T& shape) {
    // ...
}

Это мощный инструмент, но для 90% задач классические абстрактные классы — то, что нужно. Не усложняйте код без необходимости.

Практический пример: интерфейс для плагинов

Представьте, что вы пишете систему, которая загружает плагины. Каждый плагин должен уметь инициализироваться и выполнять работу. Интерфейс будет выглядеть так:

class IPlugin {
public:
    virtual void initialize() = 0                

Блоги

Рекомендаций по книгам