f1's use counter at 1
f1's use counter at 2
DTOR bar
Back to outer scope
1
first f()
call
f: use counter at 2
second f() call
f: use counter at 1
DTOR foo
end of main()
Как это работает
При создании и удалении объектов
shared_ptr
работает аналогично unique_ptr
. Создание общих указателей выглядит так же, как и создание уникальных указателей (однако существует функция make_shared
, которая создает общие объекты в дополнение к функции make_unique
для уникальных указателей unique_ptr
).Основное отличие от
unique_ptr
заключается в том, что можно копировать экземпляры shared_ptr
, поскольку общие указатели поддерживают так называемый блок управления вместе с объектом, которым они управляют. Блок управления содержит указатель на объект и счетчик ссылок или счетчик использования. Если на объект указывают N
экземпляров shared_ptr
, то счетчик использования имеет значение N
. Когда экземпляр типа shared_ptr
разрушается, его деструктор уменьшает значение этого внутреннего счетчика использования. Последний общий указатель на такой объект при разрушении снизит значение счетчика использования до 0
. В данном случае будет вызван оператор delete
для объекта! Таким образом, мы не можем допустить утечку памяти, поскольку счетчик ссылок объекта отслеживается автоматически.Чтобы проиллюстрировать эту идею, взглянем на рис. 8.2.
В шаге 1 у нас имеются два экземпляра типа
shared_ptr
, управляющих объектом типа Foo
. Значение счетчика использования установлено на 2
. Далее shared_ptr2
уничтожается, это снижает значение счетчика использования до 1
. Экземпляр Foo
пока не уничтожается, поскольку все еще существует второй общий указатель. В шаге 3
последний общий указатель также уничтожается. Это приводит к тому, что значение счетчика использования становится равным 0
. Шаг 4 выполняется сразу после шага 3. Блок управления и экземпляр типа Foo
уничтожаются, и занятая ими память возвращается в кучу.С помощью
shared_ptr
и unique_ptr
можно автоматически справиться с большинством объектов, память для которых выделяется динамически, не беспокоясь об утечках памяти. Следует, однако, рассмотреть один важный подводный камень. Представьте, что у нас имеются два объекта в куче, которые содержат общие указатели друг на друга, и какой-то другой общий указатель, указывающий на один из них откуда-то еще. Если этот внешний указатель выйдет за пределы области видимости, то счетчики использования обоих объектов все еще будут иметь ненулевые значения, поскольку ссылаются друг на друга. Это приводит к утечке памяти. В подобной ситуации общие указатели применять нельзя, поскольку цепочки таких циклических ссылок не дают снизить значение счетчика использования до 0
.
Дополнительная информация
Рассмотрим следующий код. Допустим, вам сказали, что он провоцирует потенциальную утечку памяти.
void function(shared_ptr, shared_ptr, int);
// "function" определена где-то еще
// ...далее по коду:
function(new A{}, new B{}, other_function());
Кто-то спросит: «Где же утечка?» — ведь объекты
A
и B
, для которых только что выделена память, мгновенно передаются в экземпляры типа shared_ptr
, и это позволяет обезопасить нас от утечек.Да, это так, утечки памяти нам не грозят до тех пор, пока указатели хранятся в экземплярах типа
shared_ptr
. Эту проблему решить довольно сложно.Когда мы вызываем функцию
f(x(),y(),z())
, компилятор должен собрать код, который сначала вызывает функции x()
, y()
и z()
, чтобы он мог перенаправить результат их работы в функцию f
. Нас не устраивает то, что компилятор может выполнить вызовы функций x
, y
и z
в любом порядке.Взглянем на пример еще раз и подумаем: какие события произойдут, если компилятор решит структурировать код так, что сначала будет вызвана функция
new A{}
, затем — other_function()
, а затем — B{}
, прежде чем результаты работы этих функций будут переданы далее? Если функция other_function()
сгенерирует исключение, то мы получим утечку памяти, поскольку у нас в куче все еще будет неуправляемый объект A
, поскольку мы не имели возможности передать его под управление shared_ptr
. Независимо от того, как мы обработаем исключение, дескриптор объекта пропадет, и мы не сможем удалить его!Существует два простых способа обойти эту проблему:
// 1.)
function(make_shared(), make_shared(), other_function());
// 2.)
shared_ptr ap {new A{}};
shared_ptr bp {new B{}};
function(ap, bp, other_function());
Таким образом, объекты уже попадут под управление
shared_ptr
независимо от того, где позднее будет сгенерировано исключение.Работаем со слабыми указателями на разделяемые объекты
Из примера, посвященного
shared_ptr
, мы узнали, какими полезными и простыми в использовании являются общие указатели. Вместе с unique_pt
r они предоставляют бесценную возможность по улучшению нашего кода, нуждающегося в управлении объектами, память для которых выделяется динамически.Копируя
shared_ptr
, мы увеличиваем его внутренний счетчик ссылок. До тех пор, пока мы храним нашу копию общего указателя, объект, на который он указывает, не будет удален. Но если нужно что-то вроде слабого указателя, который позволит получать объект до тех пор, пока тот существует, но не мешает его удалению? Как мы определим, существует ли еще объект?В таких ситуациях нам поможет
weak_ptr
. Использовать его чуть сложнее, чем unique_ptr
и shared_ptr
, но после прочтения этого раздела вы научитесь применять его.
Как это делается
В этом примере мы реализуем программу, которая поддерживает объекты, используя экземпляры типа
shared_ptr
, а затем добавим weak_ptr
, чтобы увидеть, как это меняет поведение при управлении памятью с помощью умного указателя.
1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространства имен
std
по умолчанию:
#include
#include
#include
using namespace std;
2. Затем реализуем класс, чей деструктор выводит на экран сообщение. Таким образом, нам будет проще проверить факт уничтожения объекта.
struct Foo {
int value;
Foo(int i) : value{i} {}
~Foo() { cout << "DTOR Foo " << value << '\n'; }
};
3. Также реализуем функцию, которая выводит на экран информацию о слабом указателе, что позволит узнавать о его состоянии в разные моменты выполнения программы. Функция
expired
класса weak_ptr
скажет о том, существует ли еще объект, на который он указывает, поскольку хранение слабого указателя на объект не продлевает его время жизни! Счетчик