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