Владение — это концепция; умные указатели — инструмент
До unique_ptr и shared_ptr владение было неявным — соглашение, записанное
в комментариях и поддерживаемое только дисциплиной. «Кто вызывает create(),
должен вызвать destroy()». Это работало до тех пор, пока не переставало:
исключения, ранние возвраты и множество путей выхода приводили к утечкам.
Умные указатели делают владение явным и проверяемым системой типов. unique_ptr<T>
не просто хранит указатель — он говорит: «Я единственный владелец этого объекта.
Когда я выхожу из области видимости, объект уничтожается».
Понимание умных указателей означает понимание трёх моделей владения, которые они кодируют:
| Тип | Модель владения |
|---|---|
unique_ptr<T> |
Единственный владелец — один указатель, одно время жизни |
shared_ptr<T> |
Разделённое владение — подсчёт ссылок, уничтожается при исчезновении последнего владельца |
weak_ptr<T> |
Невладеющий наблюдатель — не удерживает объект живым |
unique_ptr — выбор по умолчанию
unique_ptr — RAII-обёртка с нулевыми накладными расходами. На большинстве компиляторов
компилируется в тот же машинный код, что и сырой указатель с ручным delete.
1// Создание
2auto sensor = std::make_unique<Bmp280Sensor>(hi2c1);
3
4// Автоматическое уничтожение при выходе из области видимости
5// delete не нужен — даже при исключении
make_unique предпочтительнее new — он безопасен относительно исключений и
предотвращает ситуацию, когда указатель выделен, но ещё не принадлежит никому.
Передача владения
unique_ptr не копируется. Владение передаётся через std::move:
1std::unique_ptr<ISensor> createSensor(SensorType type) {
2 switch (type) {
3 case SensorType::BMP280: return std::make_unique<Bmp280Sensor>(hi2c1);
4 case SensorType::NTC: return std::make_unique<NtcSensor>(ADC_CHANNEL_3);
5 }
6}
7
8// Передача вызывающему — фабрика больше не владеет объектом
9auto sensor = createSensor(SensorType::BMP280);
10
11// Передача другому unique_ptr
12auto other = std::move(sensor);
13// sensor теперь nullptr; other владеет объектом
Фабричные функции, возвращающие unique_ptr — идиоматический способ передачи
динамически созданных объектов без раскрытия new.
Передача в функции
1// Передаёт владение — вызываемый становится владельцем
2void take(std::unique_ptr<ISensor> sensor);
3
4// Заимствует — вызываемый использует объект, но не владеет им
5void use(ISensor& sensor); // предпочтительно
6void use(ISensor* sensor); // только если nullptr — допустимый аргумент
7void use(const ISensor& sensor); // заимствование только для чтения
Передавайте по сырой ссылке или указателю, когда просто используете объект, не владея им.
Передача unique_ptr по значению передаёт владение функции — вызывающий теряет объект.
Это редко нужно в конвейере, который хранит сенсор.
shared_ptr — разделённое владение со стоимостью
shared_ptr поддерживает счётчик ссылок. Каждая копия увеличивает счётчик; каждое
уничтожение уменьшает. Когда счётчик достигает нуля, объект уничтожается.
1auto sensor = std::make_shared<Bmp280Sensor>(hi2c1);
2auto copy = sensor; // счётчик = 2
3
4// Оба — sensor и copy — могут использовать объект
5// Уничтожается, когда оба выходят из области видимости
Стоимость
shared_ptr не является нулевой стоимостью:
- Два выделения на куче по умолчанию (объект + блок управления), сокращается до одного с
make_shared - Две атомарные операции (инкремент + декремент) при копировании и уничтожении — дорого на многоядерных
- Больше, чем
unique_ptr— обычно 16 байт (указатель + указатель блока управления)
На встраиваемом Cortex-M без ОС атомарные операции всё равно добавляют накладные расходы и требуют отключения прерываний или LDREX/STREX.
Когда shared_ptr — правильный выбор
Используйте shared_ptr, когда несколько несвязанных частей системы должны удерживать
объект живым и нельзя определить единственного владельца:
1// Конфигурация загружается один раз, совместно используется между этапами конвейера
2auto config = std::make_shared<SensorConfig>(loadConfig());
3
4SensorPipeline pipeline(config);
5DiagnosticsUnit diag(config);
6Logger logger(config);
7
8// Config живёт до уничтожения всех трёх — порядок не важен
Если можно определить единственного владельца — используйте unique_ptr. shared_ptr —
не «безопасный вариант по умолчанию», а явный выбор разделённого владения с накладными
расходами времени выполнения.
weak_ptr — наблюдение без владения
weak_ptr хранит невладеющую ссылку на объект, управляемый shared_ptr. Он не
предотвращает уничтожение. Чтобы использовать объект, нужно вызвать lock() — он
возвращает shared_ptr, если объект ещё существует, или пустой shared_ptr, если
уже уничтожен.
1std::shared_ptr<Display> display = std::make_shared<TftDisplay>(hspi1);
2std::weak_ptr<Display> observer = display;
3
4// Позже, возможно из callback:
5if (auto d = observer.lock()) {
6 d->refresh(); // безопасно — объект ещё жив
7} else {
8 // display был уничтожен — пропустить
9}
Главный случай использования: разрыв циклов
Циклы shared_ptr вызывают утечки памяти — A хранит shared_ptr на B, B хранит
на A, ни один не достигает нулевого счётчика.
1// Цикл — оба утекают
2struct Node {
3 std::shared_ptr<Node> next; // ПЛОХО при циклической ссылке
4};
5
6// Исправление: одно направление — weak
7struct Node {
8 std::shared_ptr<Node> next;
9 std::weak_ptr<Node> prev; // наблюдатель, не владелец
10};
Кастомные удалители
unique_ptr и shared_ptr принимают кастомные удалители — полезно для ресурсов,
не являющихся объектами на куче:
1// Дескриптор HAL, который нужно освободить, а не удалить
2auto timer = std::unique_ptr<TIM_HandleTypeDef, decltype(&HAL_TIM_Base_DeInit)>(
3 &htim1, HAL_TIM_Base_DeInit
4);
5// HAL_TIM_Base_DeInit вызывается автоматически при выходе из области видимости
6
7// Файловый дескриптор
8auto fd = std::unique_ptr<FILE, decltype(&fclose)>(
9 fopen("log.bin", "wb"), fclose
10);
Для встраиваемых платформ этот паттерн управляет дескрипторами HAL, пинами CS SPI и другими ресурсами с парными функциями инициализации/деинициализации.
Сырые указатели — когда они корректны
Сырые указатели не являются неправильными. Они неправильны как владельцы. Как невладеющие наблюдатели они полностью идиоматичны:
1class SensorPipeline {
2 ISensor* sensor_; // сырой указатель — конвейер НЕ владеет объектом
3public:
4 // Принадлежит в другом месте (стек, unique_ptr в корне композиции)
5 explicit SensorPipeline(ISensor* sensor) : sensor_(sensor) {
6 assert(sensor != nullptr);
7 }
8};
Владение ясно из точки вызова:
1// main.cpp
2Bmp280Sensor sensor(hi2c1); // принадлежит стеку
3SensorPipeline pipeline(&sensor); // конвейер заимствует указатель
4// Оба уничтожаются при выходе из main — утечка невозможна
Это доминирующий паттерн во встраиваемой прошивке, где динамическое выделение сведено к минимуму или запрещено. Все объекты живут на стеке или в статической памяти; указатели передают невладеющие ссылки.
Владение во встраиваемой прошивке
Большинство встраиваемых кодовых баз полностью избегают динамического выделения:
Правило: никакого new/delete за пределами инициализации
В этой модели:
- Объекты живут на стеке или как
staticв области функции или файла unique_ptrиспользуется для фабричных функций при необходимости выделенияshared_ptrизбегается — атомарный подсчёт ссылок требует времени отключения прерываний- Сырые указатели передают невладеющие ссылки
- Ссылки (
&) предпочтительнее сырых указателей, когда nullptr не является допустимым значением
1// Типичный корень композиции для встраиваемых
2static Bmp280Sensor sensor(hi2c1); // статический — живёт вечно
3static MovingAvgFilter filter(8);
4static TftDisplay display(hspi1);
5
6static SensorPipeline pipeline(&sensor, &filter, &display);
Статические объекты в области файла инициализируются нулями до main() и уничтожаются
после него — гарантия стандарта C++.
Краткий справочник
Нужно единственное владение, динамическое выделение? unique_ptr
Нужно разделённое владение (несколько владельцев)? shared_ptr
Нужно наблюдать за shared_ptr без владения? weak_ptr
Объект на стеке / в статической памяти? сырая ссылка или сырой указатель
Передать функции, которая использует (не владеет)? T& или const T&
Передать функции, которая берёт владение? unique_ptr<T> по значению
Вернуть новый созданный объект? unique_ptr<T>
Что дальше
- std::variant и std::optional — типобезопасное владение неопределёнными или множественными типами
- Распределители памяти: пул и арена — избежание фрагментации кучи, когда unique_ptr недостаточно
- Внедрение зависимостей без фреймворка — взаимодействие владения с паттернами внедрения