Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI26 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Устройство умных указателей в C++
Умные указатели (smart pointers) — это один из наиболее важных инструментов современного C++. Они автоматизируют управление памятью и предотвращают утечки. Давайте разберемся, как они устроены изнутри.
Основной принцип
Умный указатель — это класс-обёртка над обычным указателем, который использует RAII (Resource Acquisition Is Initialization) для автоматического освобождения памяти.
// Примерная реализация unique_ptr
template<typename T>
class UniquePtr {
private:
T* ptr; // Хранит обычный указатель
public:
// Конструктор
explicit UniquePtr(T* p = nullptr) : ptr(p) {}
// Деструктор - здесь вся магия!
~UniquePtr() {
delete ptr; // Освобождаем память автоматически
}
// Запрещаем копирование (unique = уникальный владелец)
UniquePtr(const UniquePtr&) = delete;
UniquePtr& operator=(const UniquePtr&) = delete;
// Разрешаем перемещение (move)
UniquePtr(UniquePtr&& other) noexcept
: ptr(other.release()) {}
UniquePtr& operator=(UniquePtr&& other) noexcept {
reset(other.release());
return *this;
}
// Оператор разыменования
T& operator*() const { return *ptr; }
T* operator->() const { return ptr; }
T* get() const { return ptr; }
// Отпустить владение
T* release() {
T* temp = ptr;
ptr = nullptr;
return temp;
}
// Заменить указатель
void reset(T* p = nullptr) {
delete ptr;
ptr = p;
}
};
unique_ptr - единственный владелец
#include <memory>
void example() {
std::unique_ptr<int> ptr1(new int(42));
// ЗАПРЕЩЕНО - копирование
// std::unique_ptr<int> ptr2 = ptr1; // ОШИБКА!
// РАЗРЕШЕНО - перемещение (move)
std::unique_ptr<int> ptr2 = std::move(ptr1);
// Теперь ptr2 владеет памятью, ptr1 = nullptr
} // ptr2 выходит из scope, вызывается ~ptr2(), удаляется int
Внутренняя структура:
// unique_ptr содержит один указатель + deleter
std::unique_ptr<int> ptr;
// Размер: 8 байт (один указатель на 64-bit системе)
Свойства:
- Минимальный overhead (размер = размер обычного указателя)
- Нулевая стоимость абстракции (компилятор инлайнит)
- Полная ответственность за память
- Подходит для RAII
shared_ptr - коллективное владение
Основная идея: счётчик ссылок (reference counting).
// Примерная реализация shared_ptr
template<typename T>
class SharedPtr {
private:
T* ptr; // Указатель на объект
RefCounter* counter; // Указатель на счётчик ссылок
public:
explicit SharedPtr(T* p = nullptr)
: ptr(p), counter(new RefCounter(1)) {}
// Копирование - увеличиваем счётчик
SharedPtr(const SharedPtr& other)
: ptr(other.ptr), counter(other.counter) {
if (counter) counter->increment(); // Атомарная операция!
}
~SharedPtr() {
if (counter && counter->decrement() == 0) {
delete ptr; // Удаляем объект
delete counter; // Удаляем счётчик
}
}
// RefCounter - внутренняя структура
struct RefCounter {
std::atomic<int> count;
RefCounter(int initial = 1) : count(initial) {}
void increment() { ++count; }
int decrement() { return --count; }
};
};
Визуально:
shared_ptr ptr1(new Data);
ptr1 -> [Data объект]
ptr1 -> [RefCounter: count=1]
shared_ptr ptr2 = ptr1;
ptr1 -> [Data объект]
ptr2 -> [RefCounter: count=2] (одна структура для обоих)
~ptr1; // count = 1, объект остаётся
~ptr2; // count = 0, объект удаляется
Размер shared_ptr:
std::shared_ptr<int> ptr;
// Размер: 16 байт
// - 8 байт: указатель на объект
// - 8 байт: указатель на RefCounter
std::unique_ptr<int> ptr;
// Размер: 8 байт (только указатель)
Контрольный блок (Control Block)
В реальности, современные реализации используют контрольный блок - единую структуру для всей информации.
// Внутренняя структура std::shared_ptr
struct ControlBlock {
std::atomic<int> shared_count; // Счётчик shared_ptr
std::atomic<int> weak_count; // Счётчик weak_ptr
T* object;
Deleter deleter;
Allocator allocator;
};
// shared_ptr хранит:
class shared_ptr {
T* ptr; // Указатель на объект (для быстрого доступа)
ControlBlock* control; // Указатель на контрольный блок
};
weak_ptr - ненавязчивая ссылка
Решает проблему циклических ссылок:
struct Node {
std::shared_ptr<Node> next;
std::weak_ptr<Node> prev; // Не держит объект в памяти!
};
void problem() {
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
node1->next = node2;
node2->prev = node1; // Циклическая ссылка! БЕЗ weak_ptr утечка!
// С weak_ptr счётчик работает правильно:
// node1 удаляется -> node2 удаляется (weak_ptr не препятствует)
}
Реализация:
template<typename T>
class WeakPtr {
private:
T* ptr;
ControlBlock* control;
public:
// weak_ptr не увеличивает shared_count!
WeakPtr(const shared_ptr<T>& p)
: ptr(p.ptr), control(p.control) {
control->weak_count.fetch_add(1); // Только слабый счётчик
}
// Используем только через lock()
shared_ptr<T> lock() const {
if (control->shared_count > 0) {
return shared_ptr<T>(ptr, control);
}
return nullptr; // Объект уже удален
}
};
make_unique vs new
// Менее эффективно - 2 выделения памяти
std::unique_ptr<int> ptr1(new int(42));
// Лучше - 1 выделение памяти
std::unique_ptr<int> ptr2 = std::make_unique<int>(42);
// Аналогично для shared_ptr
std::shared_ptr<int> ptr3(new int(42)); // 2 выделения
std::shared_ptr<int> ptr4 = std::make_shared<int>(42); // 1 выделение
Почему make_ лучше:*
- Одно выделение памяти (объект + контрольный блок вместе)
- Меньше фрагментация кучи
- Исключение safe (если конструктор выбросит, нет утечки)
Пользовательский deleter
// FILE* требует fclose(), не delete
std::unique_ptr<FILE, decltype(&fclose)> file(
fopen("test.txt", "r"),
&fclose
);
// Или лямбда
auto deleter = [](int* p) {
std::cout << "Deleting " << *p;
delete p;
};
std::unique_ptr<int, decltype(deleter)> ptr(new int(42), deleter);
Производительность
// Абсолютно БЕЗ overhead
std::unique_ptr<int> ptr = std::make_unique<int>(42);
ptr->method(); // Компилятор инлайнит, это быстро как обычный указатель
// Небольшой overhead - атомарные операции
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::shared_ptr<int> ptr2 = ptr1; // ++count (атомарно)
// ~ptr2; // --count (атомарно)
Лучшие практики
- Предпочитай unique_ptr: Используй по умолчанию, он быстрее
- make_unique/make_shared: Всегда используй, лучше производительность
- Избегай циклических ссылок: Используй weak_ptr
- Не передавай владение: Передавай const ref или сырый указатель
- Никогда не создавай указатель из this: Используй std::enable_shared_from_this
class MyClass : public std::enable_shared_from_this<MyClass> {
public:
std::shared_ptr<MyClass> get_ptr() {
return shared_from_this(); // Безопасно
}
};