Как работает shared_ptr
shared_ptr — это умный указатель в C++, предоставляемый библиотекой <memory>, который реализует автоматическое управление временем жизни объекта, используя счётчик ссылок. Он обеспечивает разделённое владение объектом: несколько shared_ptr могут указывать на один и тот же объект, и объект удаляется только тогда, когда последний shared_ptr его перестаёт использовать.
🔹 Основные принципы работы
-
shared_ptr<T> управляет объектом типа T, храня в себе:
-
Указатель на сам объект (T*).
-
Счётчик ссылок (ref count), который отслеживает, сколько shared_ptr указывают на этот объект.
-
-
При создании нового shared_ptr, счётчик устанавливается в 1.
-
При копировании shared_ptr:
-
Новый указатель указывает на тот же объект.
-
Счётчик ссылок увеличивается на 1.
-
-
При уничтожении (деструкторе) shared_ptr:
-
Счётчик уменьшается на 1.
-
Когда счётчик становится равным 0, объект удаляется (delete), и память освобождается.
-
🔹 Пример использования
#include <iostream>
#include <memory>
struct MyClass {
MyClass() { std::cout << "Constructor\\n"; }
~MyClass() { std::cout << "Destructor\\n"; }
};
int main() {
std::shared_ptr<MyClass> ptr1 = std::make_shared<MyClass>(); // count = 1
{
std::shared_ptr<MyClass> 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
- **Через std::make_shared (рекомендуемый способ):
**
auto sp = std::make_shared<MyClass>();
-
Безопаснее и быстрее, чем shared_ptr(new MyClass), так как делает одну аллокацию для объекта и контрольного блока.
-
**Через конструктор:
**
std::shared_ptr<MyClass> sp(new MyClass);
- Меньше оптимизирован: может быть 2 выделения памяти (для объекта и для контрольного блока).
🔹 Преимущества
- **Автоматическое управление памятью.
** -
Безопасность: снижает вероятность утечек.
-
Удобно делиться владением объектами между разными компонентами.
🔹 Потокобезопасность
-
Инкремент и декремент счётчика ссылок — потокобезопасны.
-
Однако доступ к самому объекту через shared_ptr — не потокобезопасен, если объект не защищён вручную (например, мьютексом).
🔹 Использование с weak_ptr
Чтобы избежать циклических ссылок, shared_ptr часто используется совместно с weak_ptr.
struct B; // объявление
struct A {
std::shared_ptr<B> b_ptr;
};
struct B {
std::weak_ptr<A> a_ptr; // избегаем цикла shared_ptr
};
Если бы B содержал shared_ptr<A>, получился бы цикл: A → B → A, и ни один из объектов не удалился бы.
🔹 Специфика копирования и перемещения
-
Копирование (operator=): увеличивает счётчик ссылок.
-
Перемещение (std::move): передаёт владение, не увеличивает счётчик ссылок.
std::shared_ptr<T> a = std::make_shared<T>();
std::shared_ptr<T> b = a; // copy: count++
std::shared_ptr<T> c = std::move(a); // move: a обнуляется, count не меняется
🔹 Пользовательский deleter
Можно задать свою функцию удаления:
auto deleter = \[\](MyClass\* p) {
std::cout << "Custom delete\\n";
delete p;
};
std::shared_ptr<MyClass> sp(new MyClass, deleter);
Это полезно для работы с ресурсами, отличными от new/delete, например файлами, сокетами, ручками ОС.
🔹 Как узнать текущий счётчик
std::shared_ptr<int> sp1 = std::make_shared<int>(10);
std::shared_ptr<int> sp2 = sp1;
std::cout << sp1.use_count(); // 2
Метод .use_count() показывает число владельцев объекта.
🔹 Потенциальные проблемы
-
Циклические зависимости
- shared_ptr не справляется с циклами: объект не удалится, если A → B и B → A через shared_ptr.
-
Задержка освобождения ресурсов
- Объект будет жить, пока его последний shared_ptr не уничтожится — это может привести к задержкам очистки, особенно в сложных системах.
-
Избыточное использование
- Иногда unique_ptr предпочтительнее: если объект имеет одного владельца, shared_ptr добавляет лишние накладные расходы.
🔹 Отличие от unique_ptr
Особенность | shared_ptr | unique_ptr |
---|---|---|
Количество владельцев | Несколько (ref count) | Только один |
--- | --- | --- |
Копирование | Разрешено (увеличивает счётчик) | Запрещено (перемещаем только) |
--- | --- | --- |
Расходы | Больше: хранит счётчик + блок | Минимальные |
--- | --- | --- |
Производительность | Медленнее, особенно при частом копировании | Быстрее |
--- | --- | --- |
🔹 Когда использовать
shared_ptr уместен:
-
Когда объект должен жить до тех пор, пока хотя бы один пользователь ссылается на него.
-
Когда владение должно быть разделено между несколькими частями программы.
-
В графах, деревьях, зависимостях между компонентами.
Но следует избегать избыточного использования — особенно в простых структурах или в производительно-критичном коде.