Шрифт:
Интервал:
Закладка:
$ ./shared_ptr
Inner scope begin
CTOR foo
CTOR bar
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<A>, shared_ptr<B>, 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<A>(), make_shared<B>(), other_function());
// 2.)
shared_ptr<A> ap {new A{}};
shared_ptr<B> bp {new B{}};
function(ap, bp, other_function());
Таким образом, объекты уже попадут под управление shared_ptr независимо от того, где позднее будет сгенерировано исключение.
Работаем со слабыми указателями на разделяемые объекты
Из примера, посвященного shared_ptr, мы узнали, какими полезными и простыми в использовании являются общие указатели. Вместе с unique_ptr они предоставляют бесценную возможность по улучшению нашего кода, нуждающегося в управлении объектами, память для которых выделяется динамически.
Копируя shared_ptr, мы увеличиваем его внутренний счетчик ссылок. До тех пор, пока мы храним нашу копию общего указателя, объект, на который он указывает, не будет удален. Но если нужно что-то вроде слабого указателя, который позволит получать объект до тех пор, пока тот существует, но не мешает его удалению? Как мы определим, существует ли еще объект?
В таких ситуациях нам поможет weak_ptr. Использовать его чуть сложнее, чем unique_ptr и shared_ptr, но после прочтения этого раздела вы научитесь применять его.
Как это делается
В этом примере мы реализуем программу, которая поддерживает объекты, используя экземпляры типа shared_ptr, а затем добавим weak_ptr, чтобы увидеть, как это меняет поведение при управлении памятью с помощью умного указателя.
1. Сначала включим необходимые заголовочные файлы и объявим об использовании пространства имен std по умолчанию:
#include <iostream>
#include <iomanip>
#include <memory>
using namespace std;
2. Затем реализуем класс, чей деструктор выводит на