Как работает shared_ptr


shared_ptr — это умный указатель в C++, предоставляемый библиотекой <memory>, который реализует автоматическое управление временем жизни объекта, используя счётчик ссылок. Он обеспечивает разделённое владение объектом: несколько shared_ptr могут указывать на один и тот же объект, и объект удаляется только тогда, когда последний shared_ptr его перестаёт использовать.

🔹 Основные принципы работы

  1. shared_ptr<T> управляет объектом типа T, храня в себе:

    • Указатель на сам объект (T*).

    • Счётчик ссылок (ref count), который отслеживает, сколько shared_ptr указывают на этот объект.

  2. При создании нового shared_ptr, счётчик устанавливается в 1.

  3. При копировании shared_ptr:

    • Новый указатель указывает на тот же объект.

    • Счётчик ссылок увеличивается на 1.

  4. При уничтожении (деструкторе) shared_ptr:

    • Счётчик уменьшается на 1.

    • Когда счётчик становится равным 0, объект удаляется (delete), и память освобождается.

🔹 Пример использования

#include &lt;iostream&gt;
#include &lt;memory&gt;
struct MyClass {
MyClass() { std::cout << "Constructor\\n"; }
~MyClass() { std::cout << "Destructor\\n"; }
};
int main() {
std::shared_ptr&lt;MyClass&gt; ptr1 = std::make_shared&lt;MyClass&gt;(); // count = 1
{
std::shared_ptr&lt;MyClass&gt; ptr2 = ptr1; // count = 2
} // ptr2 уничтожен, count = 1
// ptr1 уничтожен в конце, count = 0  вызывается деструктор
}

Вывод:

Constructor
Destructor

🔹 Как устроен счётчик ссылок

Под капотом shared_ptr использует control block (контрольный блок), в котором хранится:

  • Strong reference count — число shared_ptr, владеющих объектом.

  • Weak reference count — число weak_ptr, ссылающихся на объект (без владения).

  • Указатель на сам управляемый объект.

Контрольный блок создаётся один раз и используется всеми shared_ptr и weak_ptr, связанными с объектом.

🔹 Создание shared_ptr

  1. **Через std::make_shared (рекомендуемый способ):
    **
auto sp = std::make_shared&lt;MyClass&gt;();
  • Безопаснее и быстрее, чем shared_ptr(new MyClass), так как делает одну аллокацию для объекта и контрольного блока.

  • **Через конструктор:
    **

std::shared_ptr&lt;MyClass&gt; sp(new MyClass);
  • Меньше оптимизирован: может быть 2 выделения памяти (для объекта и для контрольного блока).

🔹 Преимущества

  • **Автоматическое управление памятью.
    **
  • Безопасность: снижает вероятность утечек.

  • Удобно делиться владением объектами между разными компонентами.

🔹 Потокобезопасность

  • Инкремент и декремент счётчика ссылок — потокобезопасны.

  • Однако доступ к самому объекту через shared_ptr — не потокобезопасен, если объект не защищён вручную (например, мьютексом).

🔹 Использование с weak_ptr

Чтобы избежать циклических ссылок, shared_ptr часто используется совместно с weak_ptr.

struct B; // объявление
struct A {
std::shared_ptr&lt;B&gt; b_ptr;
};
struct B {
std::weak_ptr&lt;A&gt; a_ptr; // избегаем цикла shared_ptr
};

Если бы B содержал shared_ptr<A>, получился бы цикл: A → B → A, и ни один из объектов не удалился бы.

🔹 Специфика копирования и перемещения

  • Копирование (operator=): увеличивает счётчик ссылок.

  • Перемещение (std::move): передаёт владение, не увеличивает счётчик ссылок.

std::shared_ptr&lt;T&gt; a = std::make_shared&lt;T&gt;();
std::shared_ptr&lt;T&gt; b = a; // copy: count++
std::shared_ptr&lt;T&gt; c = std::move(a); // move: a обнуляется, count не меняется

🔹 Пользовательский deleter

Можно задать свою функцию удаления:

auto deleter = \[\](MyClass\* p) {
std::cout << "Custom delete\\n";
delete p;
};
std::shared_ptr&lt;MyClass&gt; sp(new MyClass, deleter);

Это полезно для работы с ресурсами, отличными от new/delete, например файлами, сокетами, ручками ОС.

🔹 Как узнать текущий счётчик

std::shared_ptr&lt;int&gt; sp1 = std::make_shared&lt;int&gt;(10);
std::shared_ptr&lt;int&gt; sp2 = sp1;
std::cout << sp1.use_count(); // 2

Метод .use_count() показывает число владельцев объекта.

🔹 Потенциальные проблемы

  1. Циклические зависимости

    • shared_ptr не справляется с циклами: объект не удалится, если A → B и B → A через shared_ptr.
  2. Задержка освобождения ресурсов

    • Объект будет жить, пока его последний shared_ptr не уничтожится — это может привести к задержкам очистки, особенно в сложных системах.
  3. Избыточное использование

    • Иногда unique_ptr предпочтительнее: если объект имеет одного владельца, shared_ptr добавляет лишние накладные расходы.

🔹 Отличие от unique_ptr

Особенность shared_ptr unique_ptr
Количество владельцев Несколько (ref count) Только один
--- --- ---
Копирование Разрешено (увеличивает счётчик) Запрещено (перемещаем только)
--- --- ---
Расходы Больше: хранит счётчик + блок Минимальные
--- --- ---
Производительность Медленнее, особенно при частом копировании Быстрее
--- --- ---

🔹 Когда использовать

shared_ptr уместен:

  • Когда объект должен жить до тех пор, пока хотя бы один пользователь ссылается на него.

  • Когда владение должно быть разделено между несколькими частями программы.

  • В графах, деревьях, зависимостях между компонентами.

Но следует избегать избыточного использования — особенно в простых структурах или в производительно-критичном коде.