Перейти к содержимому
Теория · Память и умные указатели · урок 8 из 12

std::shared_ptr и weak_ptr

У объекта, которым владеет std::unique_ptr, владелец всегда один. Но бывает, что объектом пользуются несколько частей программы, и заранее неизвестно, какая закончит последней. Для этого есть std::shared_ptr: его можно копировать, и все копии владеют одним объектом.

C++
#include <iostream>
#include <memory>

int main() {
    std::shared_ptr<int> a = std::make_shared<int>(7);
    std::shared_ptr<int> b = a;
    *b = 8;
    std::cout << *a << ' ' << a.use_count() << '\n';  // 8 2
    b = nullptr;
    std::cout << a.use_count() << '\n';  // 1
}

std::make_shared<int>(7) создаёт число в куче вместе с первым владельцем. Строка std::shared_ptr<int> b = a; копирует не число, а владение. a и b указывают на одно и то же число, поэтому после *b = 8 значение *a тоже равно 8.

Счётчик владельцев

Это общее владение (shared ownership): объект принадлежит всем копиям сразу. shared_ptr считает владельцев, и use_count() возвращает их число. Каждая копия увеличивает счётчик на единицу. Когда владелец уничтожается или ему присваивают nullptr, счётчик уменьшается.

Объект удаляется, когда счётчик доходит до нуля, то есть вместе с последним владельцем. Подсчёт стоит времени и памяти, поэтому shared_ptr берут, только когда владельцев действительно несколько. В остальных случаях хватает std::unique_ptr.

Слабый указатель

std::weak_ptr — слабый указатель (weak pointer): он следит за объектом, которым владеют shared_ptr, но сам им не владеет. Его создают из shared_ptr, и use_count() от этого не растёт. Метод lock() возвращает shared_ptr на объект или пустой shared_ptr, если объект уже удалён:

C++
std::shared_ptr<int> owner = std::make_shared<int>(5);
std::weak_ptr<int> watcher = owner;
std::cout << owner.use_count() << '\n';  // 1
owner = nullptr;
std::cout << (watcher.lock() == nullptr) << '\n';  // 1

Частая ошибка

Два объекта владеют друг другом через shared_ptr:

C++
#include <iostream>
#include <memory>

struct Node {
    std::shared_ptr<Node> next;
    ~Node() { std::cout << "~Node\n"; }
};

int main() {
    std::shared_ptr<Node> a = std::make_shared<Node>();
    std::shared_ptr<Node> b = std::make_shared<Node>();
    a->next = b;
    b->next = a;  // ✗
}

Стрелка работает и с умными указателями. Программа ничего не печатает. В конце main уничтожаются a и b, но каждый Node по-прежнему владеет соседом, и оба счётчика остаются равны единице. Деструкторы не вызываются, и оба объекта утекают.

Такой цикл разрывает слабый указатель. Если объявить поле как std::weak_ptr<Node> next;, оно не владеет соседом, оба счётчика доходят до нуля, и программа печатает ~Node дважды. В настоящих программах слабой обычно делают одну связь из двух — например, от дочернего объекта к родительскому.