⚙️ C++

ООП: Инкапсуляция и полиморфизм в C++: подробное руководство

Что это такое и зачем? (аналогия из реальной жизни)

Представь, что ты собираешь робота-пылесос. У него есть три главные части:

  • Мотор — он крутит щётки и создаёт всасывание.
  • Датчики — они видят стены и мебель.
  • Батарея — даёт энергию.

Когда ты нажимаешь кнопку «Старт», ты не думаешь: «Сейчас я подам напряжение 12 вольт на обмотки мотора, запущу ШИМ-контроллер и опрошу ультразвуковой дальномер». Ты просто говоришь: «Уберись в комнате». Внутри робота происходит сложная магия, но снаружи — простой интерфейс.

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

Полиморфизм — это когда один и тот же интерфейс работает по-разному в зависимости от ситуации. Представь, что у тебя есть универсальный пульт. Если ты наводишь его на телевизор — он переключает каналы. Если на кондиционер — меняет температуру. Кнопка «Вкл» одна, а результат разный.

💡 Запомни: Инкапсуляция — это про «что внутри — не твоё дело». Полиморфизм — про «один интерфейс — много реализаций».

Теория: как это работает изнутри

Инкапсуляция

В C++ инкапсуляция реализуется через модификаторы доступа:

  • private — данные видны только внутри класса. Никто снаружи не может их трогать.
  • protected — данные видны внутри класса и его наследникам.
  • public — данные видны всем.

Правило хорошего тона: все поля (переменные) делай private, а доступ к ним — через public методы (геттеры и сеттеры).

Полиморфизм

Полиморфизм бывает двух видов:

  1. Статический (раннее связывание) — решается на этапе компиляции. Это перегрузка функций и шаблоны.
  2. Динамический (позднее связывание) — решается во время выполнения программы. Используется через виртуальные функции и наследование.

Когда ты вызываешь метод у объекта, компилятор смотрит: «А какой именно метод вызвать?» Если метод виртуальный — решение откладывается до момента запуска программы.

⚠️ Важно: Без виртуальных функций полиморфизм в C++ не работает. Если ты забыл написать virtual, будет вызван метод родительского класса, даже если ты передал объект-наследник.

Синтаксис в C++: все формы и варианты

Инкапсуляция: базовый синтаксис

class РоботПылесос {
private:
    int уровеньЗаряда;  // Скрытое поле — никто не может изменить напрямую
    bool включен;

public:
    // Конструктор — создаём объект
    РоботПылесос() {
        уровеньЗаряда = 100;
        включен = false;
    }

    // Геттер — получить значение
    int получитьЗаряд() {
        return уровеньЗаряда;
    }

    // Сеттер — установить значение с проверкой
    void установитьЗаряд(int новыйЗаряд) {
        if (новыйЗаряд >= 0 && новыйЗаряд <= 100) {
            уровеньЗаряда = новыйЗаряд;
        }
    }

    // Публичный метод — интерфейс
    void нажатьСтарт() {
        if (уровеньЗаряда > 10) {
            включен = true;
            cout << "Робот начал уборку!" << endl;
        } else {
            cout << "Заряди меня!" << endl;
        }
    }
};

Полиморфизм: виртуальные функции

class Животное {
public:
    // Виртуальный метод — полиморфизм
    virtual void издатьЗвук() {
        cout << "Какое-то животное издаёт звук" << endl;
    }
};

class Собака : public Животное {
public:
    void издатьЗвук() override {  // override — проверяет, что метод действительно виртуальный
        cout << "Гав-гав!" << endl;
    }
};

class Кошка : public Животное {
public:
    void издатьЗвук() override {
        cout << "Мяу!" << endl;
    }
};

Таблица модификаторов доступа

Модификатор Внутри класса В наследнике Снаружи
private ✅ Да ❌ Нет ❌ Нет
protected ✅ Да ✅ Да ❌ Нет
public ✅ Да ✅ Да ✅ Да

Примеры кода — от простого к сложному

Пример 1: Простейшая инкапсуляция

#include <iostream>
using namespace std;

class Счетчик {
private:
    int значение;  // Скрытое поле

public:
    // Конструктор
    Счетчик() {
        значение = 0;
    }

    // Метод для увеличения
    void увеличить() {
        значение++;
    }

    // Метод для получения значения
    int получитьЗначение() {
        return значение;
    }
};

int main() {
    Счетчик мойСчетчик;
    мойСчетчик.увеличить();
    мойСчетчик.увеличить();
    // мойСчетчик.значение = 100;  // Ошибка! Поле private
    cout << "Значение: " << мойСчетчик.получитьЗначение() << endl;
    return 0;
}

Объяснение: Мы спрятали поле значение и дали только два метода: увеличить и получитьЗначение. Никто не может установить значение напрямую — только через разрешённые операции.

Пример 2: Сеттер с валидацией

class БанковскийСчет {
private:
    double баланс;

public:
    БанковскийСчет() {
        баланс = 0.0;
    }

    // Сеттер с проверкой
    void внестиДеньги(double сумма) {
        if (сумма > 0) {
            баланс += сумма;
            cout << "Внесено: " << сумма << endl;
        } else {
            cout << "Сумма должна быть положительной!" << endl;
        }
    }

    bool снятьДеньги(double сумма) {
        if (сумма > 0 && сумма <= баланс) {
            баланс -= сумма;
            return true;
        }
        return false;
    }

    double получитьБаланс() {
        return баланс;
    }
};

Объяснение: Мы не даём напрямую менять баланс. Все операции проходят через методы с проверками. Это защищает от ошибок: нельзя снять больше, чем есть, или внести отрицательную сумму.

Пример 3: Простой полиморфизм через наследование

class Фигура {
public:
    virtual double площадь() {
        return 0;
    }
};

class Круг : public Фигура {
private:
    double радиус;
public:
    Круг(double r) : радиус(r) {}

    double площадь() override {
        return 3.14159 * радиус * радиус;
    }
};

class Прямоугольник : public Фигура {
private:
    double ширина, высота;
public:
    Прямоугольник(double w, double h) : ширина(w), высота(h) {}

    double площадь() override {
        return ширина * высота;
    }
};

void вывестиПлощадь(Фигура& фигура) {
    cout << "Площадь: " << фигура.площадь() << endl;
}

int main() {
    Круг круг(5);
    Прямоугольник прямоугольник(4, 6);
    
    вывестиПлощадь(круг);          // Выведет площадь круга
    вывестиПлощадь(прямоугольник); // Выведет площадь прямоугольника
    return 0;
}

Объяснение: Функция вывестиПлощадь принимает ссылку на Фигура, но работает с любым наследником. Благодаря virtual вызывается правильный метод.

Пример 4: Абстрактный класс (чисто виртуальная функция)

class Транспорт {
public:
    virtual void двигаться() = 0;  // Чисто виртуальная функция — класс становится абстрактным
    virtual ~Транспорт() {}        // Виртуальный деструктор — обязательно для полиморфизма
};

class Автомобиль : public Транспорт {
public:
    void двигаться() override {
        cout << "Еду по дороге!" << endl;
    }
};

class Лодка : public Транспорт {
public:
    void двигаться() override {
        cout << "Плыву по воде!" << endl;
    }
};

int main() {
    // Транспорт т;  // Ошибка! Нельзя создать объект абстрактного класса
    Автомобиль авто;
    Лодка лодка;
    
    Транспорт* указатель;
    указатель = &авто;
    указатель->двигаться();  // "Еду по дороге!"
    
    указатель = &лодка;
    указатель->двигаться();  // "Плыву по воде!"
    return 0;
}

Объяснение: Абстрактный класс — это как чертёж. Нельзя создать «просто транспорт», но можно создать конкретные виды транспорта. Чисто виртуальная функция virtual void двигаться() = 0 заставляет все наследники реализовать этот метод.

Пример 5: Полиморфизм с массивом указателей

class Персонаж {
public:
    virtual void атаковать() {
        cout << "Базовая атака" << endl;
    }
    virtual ~Персонаж() {}
};

class Воин : public Персонаж {
public:
    void атаковать() override {
        cout << "Удар мечом!" << endl;
    }
};

class Маг : public Персонаж {
public:
    void атаковать() override {
        cout << "Огненный шар!" << endl;
    }
};

class Лучник : public Персонаж {
public:
    void атаковать() override {
        cout << "Выстрел из лука!" << endl;
    }
};

int main() {
    const int РАЗМЕР = 3;
    Персонаж* команда[РАЗМЕР];
    
    команда[0] = new Воин();
    команда[1] = new Маг();
    команда[2] = new Лучник();
    
    for (int i = 0; i < РАЗМЕР; i++) {
        команда[i]->атаковать();  // Каждый атакует по-своему
    }
    
    // Очищаем память
    for (int i = 0; i < РАЗМЕР; i++) {
        delete команда[i];
    }
    return 0;
}

Объяснение: Мы создаём массив указателей на базовый класс, но храним в нём объекты разных типов. При вызове атаковать() каждый объект ведёт себя по-своему. Это и есть полиморфизм в действии.

Пример 6: Инкапсуляция + полиморфизм вместе

class БазаДанных {
private:
    string подключение;
    bool соединениеАктивно;

protected:
    void логировать(string сообщение) {
        cout << "[Лог] " << сообщение << endl;
    }

public:
    БазаДанных(string строкаПодключения) {
        подключение = строкаПодключения;
        соединениеАктивно = false;
    }

    virtual void подключиться() {
        соединениеАктивно = true;
        логировать("Подключение установлено");
    }

    virtual void выполнитьЗапрос(string запрос) = 0;  // Абстрактный метод

    void отключиться() {
        соединениеАктивно = false;
        логировать("Соединение закрыто");
    }
};

class MySQL : public БазаДанных {
public:
    MySQL(string строка) : БазаДанных(строка) {}

    void выполнитьЗапрос(string запрос) override {
        cout << "MySQL выполняет: " << запрос << endl;
    }
};

class PostgreSQL : public БазаДанных {
public:
    PostgreSQL(string строка) : БазаДанных(строка) {}

    void выполнитьЗапрос(string запрос) override {
        cout << "PostgreSQL выполняет: " << запрос << endl;
    }
};

Объяснение: Здесь инкапсуляция защищает детали подключения, а полиморфизм позволяет работать с разными базами данных через единый интерфейс.

Пример 7: Шаблонный полиморфизм (статический)

template <typename T>
class Контейнер {
private:
    T данные;
public:
    Контейнер(T значение) : данные(значение) {}

    T получить() {
        return данные;
    }

    void показатьТип() {
        cout << "Тип данных: " << typeid(T).name() << endl;
    }
};

int main() {
    Контейнер<int> целое(42);
    Контейнер<double> дробное(3.14);
    Контейнер<string> текст("Привет");
    
    cout << целое.получить() << endl;    // 42
    cout << дробное.получить() << endl; // 3.14
    cout << текст.получить() << endl;   // Привет
    
    целое.показатьТип();  // int
    дробное.показатьТип(); // double
    return 0;
}

Объяснение: Это статический полиморфизм — шаблоны. Компилятор создаёт отдельные версии класса для каждого типа. Всё решается на этапе компиляции, без виртуальных функций.

Сравнение с Python: чем похоже и чем отличается

Характеристика C++ Python
Инкапсуляция Строгая: private, protected, public Соглашение: _private, __private (name mangling)
Полиморфизм Виртуальные функции, шаблоны, перегрузка Утиная типизация (duck typing)
Наследование Множественное (с осторожностью) Множественное (поддерживается)
Абстрактные классы Чисто виртуальные функции (= 0) Модуль ABC, @abstractmethod
Производительность Высокая (компилируемый) Ниже (интерпретируемый)
Сложность Выше (ручное управление памятью) Ниже (автоматическая сборка мусора)
💡 Ключевое отличие: В C++ ты должен явно указывать, что метод виртуальный. В Python все методы виртуальные по умолчанию — полиморфизм работает «из коробки».

Типичные ошибки и подводные камни

Ошибка 1: Забыл virtual

class Родитель {
public:
    void метод() { cout << "Родитель" << endl; }  // Нет virtual!
};

class Ребенок : public Родитель {
public:
    void метод() { cout << "Ребенок" << endl; }
};

int main() {
    Родитель* указатель = new Ребенок();
    указатель->метод();  // Выведет "Родитель", а не "Ребенок"!
    delete указатель;
    return 0;
}

Почему: Без virtual вызывается метод по типу указателя, а не по типу объекта.

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

class Базовый {
public:
    ~Базовый() { cout << "Деструктор базового" << endl; }
};

class Производный : public Базовый {
private:
    int* массив;
public:
    Производный() { массив = new int[100]; }
    ~Производный() { 
        delete[] массив;
        cout << "Деструктор производного" << endl; 
    }
};

int main() {
    Базовый* объект = new Производный();
    delete объект;  // Вызовется только деструктор Базового! Утечка памяти!
    return 0;
}

Решение: Всегда делай деструктор виртуальным в базовом классе, если планируешь наследование.

Ошибка 3: Нарушение инкапсуляции через друзей

class Секрет {
private:
    int пароль;
    friend class Хакер;  // Осторожно! Друзья имеют доступ ко всему
};

Совет: Используй friend только в крайнем случае. Это нарушает инкапсуляцию.

Ошибка 4: Срезка (slicing)

class Большой {
public:
    virtual void метод() {}
    int данные[100];
};

class Маленький : public Большой {
public:
    void метод() override {}
    int дополнительныеДанные[100];
};

int main() {
    Маленький объект;
    Большой копия = объект;  // Срезка! дополнительныеДанные потеряны
    return 0;
}

Решение: Используй указатели или ссылки, а не копирование объектов.

Лучшие практики и советы профессионалов

  1. Все поля — private. Исключение — только если ты пишешь библиотеку и точно знаешь, что делаешь.
  2. Используй override. Ключевое слово override (C++11) помогает компилятору проверить, что ты действительно переопределяешь виртуальный метод.
  3. Виртуальный деструктор в базовом классе. Если класс имеет хотя бы одну виртуальную функцию, деструктор должен быть виртуальным.
  4. Предпочитай композицию наследованию. «Является» (is-a) — наследование. «Имеет» (has-a) — композиция. Композиция гибче.
  5. Не делай все методы виртуальными. Виртуальные функции имеют накладные расходы (vtable). Используй их только там, где нужен полиморфизм.
  6. Используй const правильно. Методы, которые не меняют объект, помечай const.
  7. RAII (Resource Acquisition Is Initialization). Выделяй ресурсы в конструкторе, освобождай в деструкторе. Это основа безопасного кода в C++.
🔥 Золотое правило: Пиши код так, как будто его будут читать твои коллеги через год. Инкапсуляция и полиморфизм — это не про «как заставить работать», а про «как сделать понятно и безопасно».

Вопросы для самопроверки

  1. Вопрос: Что такое инкапсуляция и зачем она нужна?

    Ответ: Инкапсуляция — это сокрытие внутренних деталей реализации класса. Нужна для защиты данных, упрощения использования и возможности менять внутреннюю реализацию без изменения внешнего интерфейса.

  2. Вопрос: Какие модификаторы доступа есть в C++?

    Ответ: private (доступ только внутри класса), protected (доступ внутри класса и наследников), public (доступ всем).

  3. Вопрос: Что такое полиморфизм? Приведи пример из жизни.

    Ответ: Полиморфизм — способность объектов разных типов отвечать на одни и те же сообщения по-разному. Пример: кнопка «Старт» у машины, самолёта и корабля запускает разные процессы, но интерфейс один.

  4. Вопрос: Чем отличается статический полиморфизм от динамического?

    Ответ: Статический (перегрузка, шаблоны) решается на этапе компиляции. Динамический (виртуальные функции) — во время выполнения программы.

  5. Вопрос: Что будет, если не написать virtual перед методом в базовом классе?

    Ответ: Вызовется метод по типу указателя/ссылки, а не по типу объекта. Полиморфизм не сработает.

  6. Вопрос: Зачем нужен виртуальный деструктор?

    Ответ: Чтобы при удалении объекта через указатель на базовый класс правильно вызывался деструктор производного класса. Иначе будет утечка ресурсов.

  7. Вопрос: Что такое чисто виртуальная функция и абстрактный класс?

    Ответ: Чисто виртуальная функция — это функция без реализации (= 0). Класс, содержащий хотя бы одну такую функцию, становится абстрактным — нельзя создать его экземпляр.

  8. Вопрос: В чём разница между public и protected наследованием?

    Ответ: При public-наследовании модификаторы доступа сохраняются. При protected-наследовании public становится protected в наследнике.

  9. Вопрос: Что такое срезка (slicing) и как её избежать?

    Ответ: Срезка — потеря данных при копировании объекта производного класса в объект базового. Избегается использованием указателей или ссылок вместо копирования.

  10. Вопрос: Можно ли в C++ реализовать полиморфизм без наследования?

    Ответ: Да, через шаблоны (статический полиморфизм) или через std::variant и std::visit (посещение). Но классический динамический полиморфизм требует наследования и виртуальных функций.

Итог и что изучить дальше

Мы разобрали две важнейшие концепции ООП — инкапсуляцию и полиморфизм. Ты узнал:

  • Как прятать данные и давать контролируемый доступ к ним
  • Как заставить разные объекты работать через единый интерфейс
  • Как избегать типичных ошибок новичков
  • Чем C++ отличается от Python в реализации этих концепций

Что изучать дальше:

  1. Наследование — третья основа ООП. Узнай про множественное наследование и ромбовидную проблему.
  2. Шаблоны (templates) — мощный инструмент статического полиморфизма.
  3. Умные указатели (smart pointers) — modern C++ для безопасного управления памятью.
  4. Паттерны проектирования — фабрика, стратегия, наблюдатель — все они используют инкапсуляцию и полиморфизм.
  5. STL (Standard Template Library) — контейнеры, итераторы, алгоритмы — практическое применение всего, что ты выучил.
🎯 Главный вывод: Инкапсуляция делает код безопасным, полиморфизм — гибким. Вместе они позволяют писать программы, которые легко расширять и поддерживать. Не пытайся использовать ООП везде — но там, где оно нужно, используй правильно.