← Назад к вопросам

В каких проектах участвовал

1.0 Junior🔥 211 комментариев
#Опыт работы и проекты

Комментарии (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 (атомарно)

Лучшие практики

  1. Предпочитай unique_ptr: Используй по умолчанию, он быстрее
  2. make_unique/make_shared: Всегда используй, лучше производительность
  3. Избегай циклических ссылок: Используй weak_ptr
  4. Не передавай владение: Передавай const ref или сырый указатель
  5. Никогда не создавай указатель из 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(); // Безопасно
    }
};