ООП (объектно-ориентированное программирование) — подход, при котором программа собирается из объектов: каждый объект прячет свои данные и общается через методы. Разберу все четыре принципа ООП — инкапсуляцию, наследование, полиморфизм и абстракцию — на одном сквозном проекте: упрощенном видеоплеере на C++.
Содержание
Проект намеренно модельный: я не декодирую видеопоток и не подключаю кодеки, методы вроде play() и render() только печатают, что происходит. Настоящий плеер использует библиотеки обработки медиа (GStreamer, FFmpeg), но принципы ООП удобнее показать без этого шума.
Четыре принципа ООП кратко
Задача проекта: собрать плейлист из разных медиафайлов (видео и аудио) и проиграть их одним плеером. Классы и их принципы сведены в таблицу.
| Принцип | Что делает | Где в проекте |
|---|---|---|
| Абстракция | прячет детали за простым интерфейсом | базовый класс MediaFile: только kind/render, без кодека |
| Инкапсуляция | связывает данные с методами и скрывает состояние | Player: приватные position_/playing_, наружу — команды |
| Наследование | новый класс переиспользует базовый | VideoFile и AudioFile от MediaFile |
| Полиморфизм | один вызов — разное поведение по типу | render() через указатель MediaFile* |
Два соседних термина стоит развести сразу. Абстракция — это решение, что показать наружу, а что спрятать. Инкапсуляция — механизм, который держит данные и методы вместе и закрывает поля через private. Коротко: абстракция про интерфейс, инкапсуляция про доступ.
Минимальный рабочий пример целиком
Весь проект одним куском (файл player.cpp), разбор — ниже.
#include <iostream>
#include <string>
#include <vector>
#include <memory>
#include <utility>
// Абстракция: интерфейс медиафайла
class MediaFile {
public:
MediaFile(std::string name, int seconds)
: name_(std::move(name)), seconds_(seconds) {}
virtual ~MediaFile() = default; // виртуальный деструктор обязателен
virtual std::string kind() const = 0; // чистый виртуальный -> класс абстрактный
virtual void render() const = 0; // проиграть (модель, без кодека)
const std::string& name() const { return name_; }
int seconds() const { return seconds_; }
protected:
std::string name_;
int seconds_;
};
// Наследование: конкретные типы файлов
class VideoFile : public MediaFile {
public:
VideoFile(std::string name, int seconds, int height)
: MediaFile(std::move(name), seconds), height_(height) {}
std::string kind() const override { return "video"; }
void render() const override {
std::cout << " frames " << height_ << "p" << std::endl;
}
private:
int height_;
};
class AudioFile : public MediaFile {
public:
AudioFile(std::string name, int seconds, int kbps)
: MediaFile(std::move(name), seconds), kbps_(kbps) {}
std::string kind() const override { return "audio"; }
void render() const override {
std::cout << " sound " << kbps_ << " kbps" << std::endl;
}
private:
int kbps_;
};
// Инкапсуляция: Player прячет состояние, наружу - команды
class Player {
public:
void open(const MediaFile* file) {
file_ = file;
position_ = 0;
playing_ = false;
std::cout << "open " << file_->kind() << " \"" << file_->name()
<< "\" (" << file_->seconds() << " s)" << std::endl;
}
void play() {
if (!file_) return;
playing_ = true;
std::cout << "play: " << file_->name() << std::endl;
file_->render(); // полиморфизм по типу файла
}
void pause() {
playing_ = false;
std::cout << "pause at " << position_ << " s" << std::endl;
}
void stop() {
playing_ = false;
position_ = 0;
std::cout << "stop" << std::endl;
}
private:
const MediaFile* file_ = nullptr; // приватное состояние
int position_ = 0;
bool playing_ = false;
};
// Плейлист: коллекция медиафайлов за общим интерфейсом
class Playlist {
public:
void add(std::unique_ptr<MediaFile> file) {
items_.push_back(std::move(file));
}
void playAll(Player& player) const {
for (const auto& item : items_) {
player.open(item.get());
player.play();
}
}
private:
std::vector<std::unique_ptr<MediaFile>> items_;
};
int main() {
Playlist list;
list.add(std::make_unique<VideoFile>("intro.mp4", 30, 1080));
list.add(std::make_unique<AudioFile>("theme.mp3", 90, 320));
Player player;
list.playAll(player); // разное поведение render()
player.pause();
player.stop();
return 0;
}
Сборка и запуск (g++, C++17):
g++ -std=c++17 player.cpp -o player && ./player
Ожидаемый вывод:
open video "intro.mp4" (30 s)
play: intro.mp4
frames 1080p
open audio "theme.mp3" (90 s)
play: theme.mp3
sound 320 kbps
pause at 0 s
stop
playAll вызывает у обоих файлов один и тот же render(), но у видео печатаются кадры, а у аудио — битрейт. Это полиморфизм в действии.
Абстракция: интерфейс медиафайла
Класс MediaFile объявляет, что умеет любой файл, но не говорит как. Два метода помечены = 0 — это чистые виртуальные методы. Класс хотя бы с одним таким методом становится абстрактным: создать его объект напрямую нельзя, только унаследоваться и реализовать методы. В C++ нет отдельного слова interface: роль интерфейса играет абстрактный класс (у нашего MediaFile есть и общие поля name_/seconds_, поэтому это абстрактный базовый класс, а не чистый интерфейс).
Попытка создать объект абстрактного класса — типовая ошибка новичка (неверный код, результат, исправление):
MediaFile file("intro.mp4", 30); // попытка создать объект интерфейса
Компилятор g++ откажется собирать:
error: cannot declare variable 'file' to be of abstract type 'MediaFile'
note: because the following virtual functions are pure within 'MediaFile'
Исправление: создавать конкретный тип и хранить его через указатель на базовый класс.
std::unique_ptr<MediaFile> file = std::make_unique<VideoFile>("intro.mp4", 30, 1080);
Наследование: видео и аудио
VideoFile и AudioFile объявлены как : public MediaFile и переиспользуют конструктор и поля базы, а свои kind() и render() помечают словом override. Оно не обязательно, но полезно: при несовпадении сигнатуры с методом базы компилятор поймает ошибку сразу. Каждый наследник добавляет свои приватные данные — высоту кадра или битрейт.
Полиморфизм: один вызов — разное поведение
Полиморфизм работает через виртуальные методы и указатель (или ссылку) на базовый класс. Когда Player::play() вызывает file_->render(), статический тип file_ — это const MediaFile*, но реальный объект за ним — VideoFile или AudioFile, и C++ на этапе выполнения выбирает реализацию по фактическому типу (динамическая диспетчеризация).
Важная граница: диспетчеризация работает только при вызове через указатель или ссылку на базовый тип. Если положить наследника в контейнер по значению, произойдет срезка (object slicing) — производная часть потеряется. Поэтому Playlist хранит std::vector<std::unique_ptr<MediaFile>>, а не std::vector<MediaFile>; unique_ptr заодно сам освобождает память.
Здесь же ясно, зачем деструктор базового класса помечен virtual. Без этого удаление наследника через указатель на базу — неопределенное поведение: вызвался бы только деструктор базовой части. Виртуальный деструктор гарантирует вызов деструктора фактического типа.
Инкапсуляция: класс Player прячет состояние
Player хранит три приватных поля: текущий файл, позицию и флаг воспроизведения. Снаружи к ним не добраться — доступны только команды open, play, pause, stop. Это и есть инкапсуляция: пользователь класса нажимает кнопки, но не может испортить состояние напрямую. Такой контракт дает свободу менять реализацию: сегодня position_ — просто число, завтра за ним реальный таймер, а внешний код не изменится.
Границы модели
Это учебная модель, и ее пределы стоит назвать явно. render() ничего не декодирует — он печатает строку вместо работы с реальным потоком байтов. В настоящем плеере тут стоит конвейер обработки медиа: демультиплексор контейнера, декодер кодека (H.264, AAC и подобные), синхронизация аудио и видео. Но сами классы, наследование и виртуальные методы устроены так же, как в примере выше.
Выводы
- Четыре принципа удобно закрепить на одном проекте: абстракция задает интерфейс
MediaFile, наследование даетVideoFile/AudioFile, полиморфизм включается при вызовеrender()через указатель, инкапсуляция прячет состояниеPlayer. - Абстрактный класс в C++ — это класс хотя бы с одним чистым виртуальным методом (
= 0); объект такого класса создать нельзя, отдельного словаinterfaceв языке нет. - Полиморфизм работает только через указатель или ссылку на базовый тип; хранение наследников по значению приводит к срезке, поэтому в контейнере держат
unique_ptr. - Виртуальный деструктор в базе обязателен, если объектами управляют через указатель на базу.
Где применяется / связь с практикой
ООП — основа промышленного кода на C++: движки, драйверы, GUI, обработка медиа. Разобранный видеоплеер показывает тот же скелет, что и в настоящих плеерах, только без слоя кодеков. Уверенное владение классами, виртуальными методами и умными указателями — база, с которой начинается серьезная работа на C++.
Освойте тему на практике
Системно пройти путь от синтаксиса до проектов на классах помогает курс C++ Basic. Посмотреть, как устроены занятия, можно на открытых уроках Otus — вход бесплатный.
FAQ
Чем абстрактный класс отличается от интерфейса в C++?
В C++ нет ключевого слова interface; интерфейс моделируют абстрактным классом с одними чистыми виртуальными методами и без данных. Абстрактный класс, в отличие от чистого интерфейса, может хранить общие поля и реализованные методы, как MediaFile в примере.
Зачем деструктор базового класса делать виртуальным?
Чтобы при удалении объекта через указатель на базу вызвался деструктор фактического класса, а не только базовой части. Без virtual это неопределенное поведение и потенциальная утечка ресурсов наследника.
Чем override отличается от перегрузки (overload)?
override — это переопределение: метод наследника с той же сигнатурой заменяет виртуальный метод базы. Перегрузка (overload) — несколько методов с одним именем, но разными параметрами в одном классе; к наследованию она отношения не имеет.



