← Назад к курсу

Основы ООП на C++

📌 Описание

В этой статье разберем основные концепции объектно‑ориентированного программирования (ООП) на примере языка C++. Мы рассмотрим, что такое классы и объекты, четыре базовых принципа ООП, а также практические примеры, полезные паттерны и лучшие практики, которые помогут вам писать чистый, расширяемый и безопасный код.


📚 Содержание урока

Тема Что будем делать
1️⃣ Что такое ООП? Общие определения, преимущества и сравнение с процедурным стилем
2️⃣ Четыре принципа ООП Инкапсуляция, наследование, полиморфизм, абстракция
3️⃣ Классы и объекты в C++ Синтаксис, члены, конструкторы/деструкторы, области видимости
4️⃣ Пример простого класса Car – демонстрация полей, методов, доступа
5️⃣ Наследование Одиночное, виртуальное, protected/private наследование, виртуальные функции
6️⃣ Полиморфизм Перегрузка функций/операторов, динамический полиморфизм через виртуальные методы
7️⃣ Инкапсуляция private + геттеры/сеттеры, friend, mutable, const‑правильность
8️⃣ Абстракция Абстрактные классы, чисто виртуальные функции, интерфейсы
9️⃣ RAII и умные указатели unique_ptr, shared_ptr, weak_ptr, правило 3/5/0
🔟 Шаблоны и обобщенное программирование Простые и сложные шаблоны, специализация, constexpr
1️⃣1️⃣ Практический проект Иерархия Animal с наследованием, полиморфизмом и переопределением методов
1️⃣2️⃣ Лучшие практики и SOLID const‑корректность, Rule of Zero/Three/Five, принципы SOLID
1️⃣3️⃣ Отладка и профилирование Популярные инструменты (gdb, valgrind, perf), рекомендации по использованию
1️⃣4️⃣ Упражнения Задачи для закрепления материала (задачи 1‑5)

📝 Подробное изложение

1️⃣ Что такое ООП?

Объектно‑ориентированное программирование – парадигма, в которой реальный мир моделируется через объекты, содержащие данные (атрибуты) и поведение (методы). В C++ ООП реализуется через классы – шаблоны для объектов, и объекты – экземпляры классов.

Плюсы ООП:

  • Повторное использование кода (наследование)
  • Инкапсуляция деталей реализации
  • Полиморфизм упрощает расширение системы
  • Логическая структура, близкая к мышлению человека

2️⃣ Четыре принципа ООП

Принцип Описание Пример в C++
Инкапсуляция Сокрытие внутренних деталей, предоставление только нужного API class Car { private: std::string engine; public: std::string getEngine() const; };
Наследование Создание новых типов, расширяющих возможности существующих class SportsCar : public Car { … };
Полиморфизм Одно и то же имя может обозначать разное поведение в зависимости от типа Виртуальный метод virtual void drive() = 0;
Абстракция Выделение общих характеристик и упрощение модели Абстрактный базовый класс class Animal { virtual void speak() = 0; };

3️⃣ Классы и объекты

class Car {
public:
    // Конструктор
    Car(const std::string& brand, const std::string& model);
    
    // Деструктор (по умолчанию вызывается)
    ~Car();
    
    // Методы
    void drive();
    void stop();
    
    // Атрибуты (можно в public, но лучше в private)
    std::string brand;
    std::string model;
    
private:
    // Инкапсулированное поле
    int speed;
};

// Конструктор
Car::Car(const std::string& brand, const std::string& model)
    : brand(brand), model(model), speed(0) {}

// Деструктор
Car::~Car() {
    // Очистка ресурсов, если они есть
}
  • public / private / protected – контролируют доступ.
  • Конструктор – инициализирует объект, может принимать параметры.
  • Деструктор – освобождает ресурсы (RAII).

4️⃣ Пример простого класса Car

Компонент Код Комментарий
Конструктор Car::Car(const std::string& brand, const std::string& model) Задает марку и модель, инициализирует скорость 0
Метод drive() cpp void Car::drive() { speed = 50; std::cout << brand << " " << model << " едет со скоростью " << speed << " км/ч\n"; } Показывает, как изменять внутреннее состояние
Метод stop() cpp void Car::stop() { speed = 0; std::cout << brand << " " << model << " остановилась.\n"; } Сбрасывает скорость
int main() {
    Car myCar("Tesla", "Model S");
    myCar.drive();   // Tesla Model S едет со скоростью 50 км/ч
    myCar.stop();    // Tesla Model S остановилась.
}

5️⃣ Наследование

class Vehicle {
public:
    virtual void start() = 0;   // чисто виртуальный метод
    virtual void stop()  = 0;
};

class Car : public Vehicle {
public:
    void start() override { std::cout << "Ключ заведён, машина заведена.\n"; }
    void stop()  override { std::cout << "Машина остановилась.\n"; }
};

class Bicycle : public Vehicle {
public:
    void start() override { std::cout << "Поднял педали, поехал.\n"; }
    void stop()  override { std::cout << "Остановился.\n"; }
};
  • public наследование – публичные члены базового класса остаются публичными.
  • protected – члены доступны внутри дочерних классов.
  • private – ограничивает наследование, дочерний класс видит только публичные методы.
  • Виртуальный деструктор обязателен, если базовый класс имеет виртуальные функции и используется через указатель/ссылку.

6️⃣ Полиморфизм

Vehicle* v1 = new Car();
Vehicle* v2 = new Bicycle();

v1->start(); // "Ключ заведён, машина заведена."
v2->start(); // "Поднял педали, поехал."
  • Статический (перегрузка) – одно и то же имя функции, но разные сигнатуры (void add(int, int); vs void add(double, double);).
  • Динамический (виртуальный) – virtual функции, реализуемые в потомках, вызываются через указатель/ссылку на базовый тип.

7️⃣ Инкапсуляция

class Account {
private:
    double balance;
public:
    Account(double initial = 0.0) : balance(initial) {}
    void deposit(double amount) { balance += amount; }
    double getBalance() const { return balance; }   // геттер
    // Сеттер с проверкой
    void withdraw(double amount) {
        if (amount > balance) throw std::runtime_error("Недостаточно средств");
        balance -= amount;
    }
};
  • private гарантирует, что внешний код не может напрямую изменить balance.
  • friend позволяет выдать доступ ограниченному коду.
  • const — член функции не изменяет объект, позволяет вызывать её на константных экземплярах.

8️⃣ Абстракция

class Shape {
public:
    virtual double area() const = 0;   // чисто виртуальный метод
    virtual void draw() const = 0;
};

class Circle : public Shape {
private:
    double radius;
public:
    Circle(double r) : radius(r) {}
    double area() const override { return 3.14159 * radius * radius; }
    void draw() const override { std::cout << "Рисуем круг радиуса " << radius << "\n"; }
};
  • Через абстракцию мы можем работать с Shape* независимо от конкретного типа, не зная его деталей.

9️⃣ RAII и умные указатели

class File {
    std::FILE* f;
public:
    explicit File(const char* path, const char* mode) : f(std::fopen(path, mode)) {
        if (!f) throw std::runtime_error("Не удалось открыть файл");
    }
    ~File() { if (f) std::fclose(f); }

    // Другие методы для чтения/записи
};

// Использование умного указателя
std::unique_ptr<File> log = std::make_unique<File>("log.txt", "w");
// При выходе log из области видимости файл будет закрыт автоматически
  • unique_ptr – владеет ресурсом единственным владельцем.
  • shared_ptr – несколько владельцев, подсчёт ссылок.
  • weak_ptr – не владеет, нужен для избежания циклических ссылок.

Правило Zero/Three/Five – если класс управляет ресурсом, реализуйте деструктор, конструктор копирования и оператор присваивания, либо удалите их, если хотите, чтобы компилятор сгенерировал их по умолчанию.

🔟 Шаблоны и обобщенное программирование

template <typename T>
T max(const T& a, const T& b) {
    return (a > b) ? a : b;
}

// Параметризованный контейнер
template <typename K, typename V>
class SimpleMap {
    std::unordered_map<K, V> storage;
public:
    void set(const K& key, const V& value) { storage[key] = value; }
    V get(const K& key) const { return storage.at(key); }
};
  • Шаблоны позволяют писать обобщённый код, который работает с любыми типами, при этом компилятор инстанцирует специализации.

1️⃣1️⃣ Практический проект: иерархия Animal

class Animal {
public:
    virtual void speak() const = 0;          // чисто виртуальный метод
    virtual void eat() const = 0;
    virtual ~Animal() = default;            // виртуальный деструктор
};

class Dog : public Animal {
public:
    void speak() const override { std::cout << "Гав!\n"; }
    void eat() const override { std::cout << "Собака ест корм.\n"; }
};

class Cat : public Animal {
public:
    void speak() const override { std::cout << "Мяу!\n"; }
    void eat() const override { std::cout << "Кошка ест рыбу.\n"; }
};

void zoo(const Animal& animal) {
    animal.speak();
    animal.eat();
}

int main() {
    Dog dog;
    Cat cat;

    zoo(dog);   // Гав! Собака ест корм.
    zoo(cat);   // Мяу! Кошка ест рыбу.
}
  • Полиморфизм работает через указатель/ссылку на базовый Animal.
  • Каждый класс реализует свои варианты поведения.

1️⃣2️⃣ Лучшие практики (SOLID)

Принцип Что делать Пример
SRP (Single Responsibility Principle) Каждый класс отвечает за одну задачу Car не отвечает за логирование, а Logger за него
OCP (Open/Closed) Добавляйте новые функции через наследование/полиморфизм Новый SportsCar расширяет Car, не меняя её код
LSP (Liskov Substitution) Потомки должны быть заменяемыми на предка Vehicle* может указывать как на Car, так и на Bicycle
ISP (Interface Segregation) Делайте интерфейсы небольшими и специализированными IDriveable и IBrake вместо одного большого Vehicle
DIP (Dependency Inversion) Зависимость от абстракций, а не от конкретных классов class Car { std::unique_ptr<Engine> engine; };

1️⃣3️⃣ Отладка и профилирование

  • gdb – отладчик командной строки; удобно ставить брейк‑поинты (break, step, next).
  • valgrind --leak-check=full – проверка утечек памяти.
  • perf – профилирование CPU, измерение времени выполнения функций.
  • clang-tidy – статический анализатор, выявляющий проблемы стиля и безопасности.

1️⃣4️⃣ Упражнения

  1. Создайте класс Rectangle, содержащий ширину и высоту. Реализуйте метод area() и метод scale(double factor).
  2. Напишите шаблон swap<T> для обмена значениями двух переменных.
  3. Реализуйте иерархию Shape, где Circle, Square, Triangle наследуют абстрактный класс Shape. Добавьте метод draw() для каждого и функцию printAll(const std::vector<std::unique_ptr<Shape>>& shapes).
  4. Создайте умный указатель SmartArray<T> на основе std::unique_ptr, который хранит массив фиксированного размера и предоставляет оператор [].
  5. Примените принцип DIP: разработайте систему Logger, где FileLogger и ConsoleLogger реализуют общий интерфейс ILogger. Показать, как подключить любой ILogger к Application.