«Умные» указатели
Классы могут быть очень большими или экземпляров класса может быть очень много, в результате мы вынуждены создавать объекты в динамической памяти, т. к. размер стека ограничен. В разд. 13.14 мы уже рассматривали способы динамического создания объектов с помощью оператора new . В этом разделе мы рассмотрим проблемы при работе с динамическими объектами, а также научимся эти проблемы решать.
Итак, давайте создадим объект класса B внутри блока:
Указатель pB создан внутри блока (области, ограниченной фигурными скобками), следовательно, он является локальной переменной, область видимости которой ограничена блоком. После блока указатель pB уже не существует. В результате память под указатель будет освобождена, а вот динамическая память автоматически освобождена не будет и мы получим утечку памяти. Заметьте, что деструктор объекта не вызывается. Попробуйте вместо блока использовать бесконечный цикл while и понаблюдайте за размером памяти в Диспетчере задач Windows. Так вы очень быстро поймете, к чему может привести утечка памяти.
Чтобы избежать утечки памяти нужно динамическую память освобождать явным образом. Как вы уже знаете, для освобождения динамической памяти предназначен оператор delete . Давайте переделаем предыдущий пример и исправим ошибку:
Итак, указатель после выхода из блока удаляется, динамическая память освобождается и вызывается деструктор. Вроде все хорошо, но это не так! Проблема заключается в том, что при выполнении операций с объектом может произойти ошибка, которая сгенерирует исключение. В результате поток управления не дойдет до оператора delete , динамическая память освобождена не будет и деструктор вызван не будет. В результате опять получаем утечку памяти:
Результат (заметьте, что деструктор вызван не был):
Для решения проблем с освобождением динамической памяти предназначены классы unique_ptr , shared_ptr и weak_ptr , которые называются «умными» указателями. Прежде чем использовать классы не забудьте подключить заголовочный файл memory :
Класс unique_ptr: уникальный указатель
Шаблонный класс unique_ptr реализует уникальный указатель на объект. Такой указатель нельзя копировать, но можно передавать владение им. При удалении объекта указателя автоматически вызывается деструктор, внутри которого выполняется освобождение динамической памяти. Объявления класса unique_ptr :
Управление одним объектом
Создать экземпляр класса unique_ptr можно следующими основными способами:
После названия класса unique_ptr внутри угловых скобок задается название класса объекта или другой тип данных, например, int . Первые две инструкции создают объекты с нулевым указателем, а последняя инструкция при создании сразу принимает право владения экземпляром класса B , созданном в динамической памяти. Объекты «умных» указателей создаются в локальной области видимости, поэтому при выходе из области видимости будет вызван деструктор «умного» указателя, внутри которого будет освобождена динамическая память и вызван деструктор объекта. Теперь даже при возникновении исключения динамическая память будет освобождена:
Результат (деструктор был вызван):
Если закомментировать инструкцию, генерирующую исключение, то результат будет таким (деструктор был вызван):
Тут существует еще одна проблема. При динамическом создании объекта может возникнуть исключение еще до получения права владения. Чтобы этого избежать следует воспользоваться функцией make_unique() . Обратите внимание: функция доступна, начиная со стандарта C++14. Прототипы функции:
При объявлении объекта удобно использовать ключевое слово auto :
Класс unique_ptr перегружает операторы -> (доступ к члену) и * (разыменование), а также операторы сравнения:
Класс unique_ptr реализует уникальный указатель на объект. Такой указатель нельзя копировать, но можно передавать владение им другому «умному» указателю с помощью оператора присваивания и функции move() :
Перечислим основные методы класса unique_ptr :
- get() — возвращает обычный указатель или нулевой указатель, при этом сохраняя право на владение объектом:
- release() — возвращает обычный указатель или нулевой указатель, при этом передавая право на владение объектом. За освобождение динамической памяти «умный» указатель больше не отвечает, поэтому нужно выполнить освобождение самостоятельно:
- swap() — меняет местами значения двух объектов:
Вместо метода swap() можно воспользоваться функцией swap() :
- reset() — освобождает динамическую память и обнуляет указатель:
Управление массивом
Класс unique_ptr можно также использовать для работы с динамическими массивами, но следует учитывать, что по умолчанию вызывается оператор delete для одного объекта, а не для массива. Чтобы вызывался оператор delete для массива нужно внутри угловых скобок после типа указать квадратные скобки:
Пример создания динамического массива с помощью функции make_unique() :
Как видно из примера, для доступа к элементу массива используются квадратные скобки, внутри которых указывается индекс элемента внутри массива.
Класс shared_ptr: совместно используемый указатель
Шаблонный класс shared_ptr реализует совместно используемый указатель. Такой указатель можно и копировать, и перемещать. Количество копий отслеживается с помощью счетчика. Когда счетчик становится равным нулю, выполняется освобождение динамической памяти. Объявление класса shared_ptr :
Управление одним объектом
Создать экземпляр класса shared_ptr можно следующими основными способами (полный список смотрите в документации):
После названия класса shared_ptr внутри угловых скобок задается название класса объекта или другой тип данных, например, int . Первые две инструкции создают объекты с нулевым указателем, а третья инструкция при создании сразу принимает право владения экземпляром класса B , созданном в динамической памяти. Четвертая инструкция создает копию «умного» указателя. В результате счетчик ссылок у обоих указателей будет равен 2 . Получить значение счетчика позволяет метод use_count() . В пятой инструкции создается указатель класса unique_ptr , а в шестой инструкции выполняется передача владения объектом указателю класса shared_ptr . При этом объект uptr будет хранить нулевой указатель.
Когда счетчик становится равным нулю, выполняется освобождение динамической памяти, причем только один раз:
При динамическом создании объекта может возникнуть исключение еще до получения права владения. Чтобы этого избежать следует воспользоваться функцией make_shared() . Прототип функции:
Класс shared_ptr перегружает операторы -> (доступ к члену) и * (разыменование), а также операторы сравнения:
Кроме того, выполнена перегрузка оператора << , позволяющего вывести результат выполнения метода get() на консоль:
Класс shared_ptr реализует совместно используемый указатель. Такой указатель можно и копировать, и перемещать:
Перечислим основные методы класса shared_ptr :
- use_count() — возвращает количество объектов, совместно использующих указатель (возвращает значение счетчика):
- unique() — возвращает значение true , если значение счетчика равно 1 , и false — в противном случае:
- get() — возвращает обычный указатель (или нулевой указатель), при этом сохраняя право на владение объектом:
- swap() — меняет местами значения двух объектов:
Вместо метода swap() можно воспользоваться функцией swap() :
- reset() — уменьшает значение счетчика на 1 и обнуляет указатель в текущем объекте. Если значение счетчика будет равно 0 , то освобождает динамическую память. Если в качестве параметра передан указатель, то «умный» указатель получает право собственности на него:
Для приведения типов можно воспользоваться следующими функциями:
- static_pointer_cast() — стандартное приведение типов ( static_cast ). Прототип функции:
- dynamic_pointer_cast() — выполняет приведение типов указателей или ссылок ( dynamic_cast ). Применяется для приведения полиморфных типов. Прототип функции:
- const_pointer_cast() — приведение const_cast . Прототип функции:
- reinterpret_pointer_cast() — приведение reinterpret_cast . Функция доступна, начиная со стандарта C++17. Прототип функции:
Управление массивом
Начиная со стандарта C++17, класс shared_ptr можно также использовать для работы с динамическими массивами. Чтобы вызывался оператор delete для массива нужно внутри угловых скобок после типа указать квадратные скобки:
Как видно из примера, для доступа к элементу массива используются квадратные скобки, внутри которых указывается индекс элемента внутри массива.
Класс weak_ptr: «слабый» указатель
При использовании класса shared_ptr можно получить циклическую зависимость, если первый объект ссылается на второй объект, а второй на первый, или объект ссылается сам на себя. Счетчик в данном случае никогда не станет равным нулю, а, следовательно, не будет освобождена динамическая память. Давайте рассмотрим пример. Предположим, у нас есть класс C :
Если мы выполним следующие инструкции, то получим циклическую зависимость:
Решением проблемы являются «слабые» указатели, реализуемые с помощью класса weak_ptr . Достаточно в классе C объявить поле ptr с типом weak_ptr :
Класс weak_ptr владельцем указателя не считается, поэтому при использовании следующих инструкций никакой циклической зависимости не будет:
Объявление класса weak_ptr :
Создать экземпляр класса weak_ptr можно следующими основными способами (полный список смотрите в документации):
После названия класса weak_ptr внутри угловых скобок задается название класса объекта или другой тип данных, например, int . Первая инструкция создает объект с нулевым указателем, а вторая инструкция создает копию объекта. В третьей инструкции создается объект класса shared_ptr , а в четвертой инструкции на его основе создается объект класса weak_ptr . В пятой инструкции передаются права на владение объектом, при этом объект ptr3 будет хранить нулевой указатель.
Объект класса weak_ptr является оболочкой над объектом класса shared_ptr . Правом владения указателя и ответственным за освобождение динамической памяти является объект класса shared_ptr . Использовать объект напрямую нельзя, т. к. класс weak_ptr не перегружает операторы -> и * , а также не содержит метода get() . Чтобы получить доступ, нужно выполнить преобразование в объект класса shared_ptr . Сделать это можно двумя способами:
- передать объект класса weak_ptr конструктору класса shared_ptr . Если объект класса weak_ptr пустой, то генерируется исключение. Пример:
- с помощью метода lock() . Если объект класса weak_ptr пустой, то возвращается пустой объект класса shared_ptr . Пример:
С помощью оператора = можно создать копию объекта класса weak_ptr , а также присвоить объект класса shared_ptr . Начиная со стандарта C++14, существует возможность перемещения:
Класс weak_ptr содержит следующие основные методы:
- use_count() — возвращает количество объектов, совместно использующих указатель (возвращает значение счетчика):
- expired() — возвращает значение true , если значение счетчика равно 0 , и false — в противном случае:
- lock() — возвращает объект класса shared_ptr . Если объект класса weak_ptr пустой, то возвращается пустой объект класса shared_ptr :
- swap() — меняет местами значения двух объектов:
Вместо метода swap() можно воспользоваться функцией swap() :
- reset() — делает объект пустым:

Учебник C++ (Qt Creator и MinGW) в формате PDF
Помощь сайту
ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов
Умные указатели. Классы указателей unique_ptr , shared_ptr , weak_ptr
Перед изучением данной темы рекомендуется ознакомиться со следующей темой:
Содержание
- 1. Использование умных указателей в C++
- 2. Виды умных указателей
- 2.1. Указатель unique_ptr
- 2.2. Указатель shared_ptr
- 2.3. Указатель weak_ptr
Поиск на других ресурсах:
1. Использование умных указателей в C++
В современном C++ стандартная библиотека (Standard Library) включает в себя так называемые «умные указатели» (smart pointers). Эти указатели предназначены для того, чтобы безопасно (без исключительных ситуаций) обеспечить освобождение памяти и избежать утечки ресурсов.
Умные указатели определены в пространстве имен std и в заголовочном файле <memory> .
Основная цель использования умных указателей – обеспечить получение ресурса одновременно с инициализацией объекта в одной строке кода (а не в нескольких, как это было раньше с использованием так называемых обычных необработанных (raw) указателей).
На практике это выглядит так. Выделенный в стеке объект получает право владения на ресурс из кучи. Этим ресурсом может быть, например, динамически выделенная память, дестриптор объекта и т.п. Объект, получающий право на ресурс, содержит деструктор, в котором реализован код для удаления или освобождения этого ресурса или его очистки.
Если нужно инициализировать необработанный (raw) указатель по адресу реального ресурса или дескриптору ресурса, то рекомендуется передавать этот необработанный указатель умному указателю. Чтобы избежать невидимых ошибок при выделении/освобождении памяти, рекомендуется использовать необработанные указатели (raw pointers) в небольших блоках кода с ограниченной областью действия, циклах или вспомогательных функциях. В небольших блоках кода использование необработанных указателей дает максимальную производительность и уменьшает неразбериху во владении памятью.
Подобная идиома реализована в языках C#, Java.
2. Виды умных указателей
Ниже представлены виды умных указателей, которые нужно использовать для того, чтобы инкапсулировать простые старые (необработанные) указатели C++.
2.1. Указатель unique_ptr
Указатель типа unique_ptr объявляется в пространстве имен std . Обращение к указателю может быть std::unique_ptr .
Этот указатель может инкапсулировать (обладать) только один указатель старого типа. Как правило, этот указатель используется, если нет необходимости использовать указатели типа shared_ptr .
Указатель unique_ptr может быть передан (перемещен) новому владельцу, но не скопирован или разделен для общего доступа. Он заменяет уже устаревший auto_ptr . Этот указатель небольшой по размеру (один указатель) и поддерживает rvalue-ссылку.
Данный указатель используется для быстрой вставки и извлечения данных из коллекций C++.В наиболее общем случае объявление указателя unique_ptr выглядит следующим образом:
- ptr – указатель типа unique_ptr ;
- rawPtr – необработанный (raw) указатель на тип T ;
- T – тип значения, на которое указывают указатели ptr и rawPtr .
Указатель типа unique_ptr не может быть присвоен другому указателю типа unique_ptr . Это означает, что присваивание вида
вызовет ошибку на этапе компиляции. То же самое касается случая объявления указателя и его инициализации.
Указатель типа unique_ptr имеет следующие основные методы:
- get() – возвращает стандартный указатель на объект на основе указателя unique_ptr без передачи прав владения на этот объект. После вызова метода указатель unique_ptr продолжает указывать на тот же объект;
- release() – возвращает стандартный указатель на основе указателя unique_ptr с передачей прав владения. После вызова метода значение указателя unique_ptr равно nullptr ;
- reset() – сбрасывает право владения на объект. После вызова метода значение указателя unique_ptr равно nullptr .
2.2. Указатель shared_ptr
Указатель shared_ptr реализован в пространстве имен std. Полное обращение к указателю std::shared_ptr .
Это умный указатель с подсчетом количества ссылок. Данный указатель используется, когда нужно назначить один необработанный указатель нескольким владельцам. Например, при возврате копии объекта из контейнера при условии сохранения оригинала.
Указатель shared_ptr реализует подсчет количества ссылок на ресурс. Ресурс освобождается только тогда, когда количество ссылок на него будет равно 0. По такому правилу работает сборщик мусора.В наиболее общем случае создание указателя типа shared_ptr может производиться одним из двух способов:
- ptr – имя указателя типа shared_ptr , который нужно создать;
- T – тип данных, на который указывает указатель;
- rawPtr – стандартный указатель на данные типа T (объект типа T ). Этот указатель служит в качестве исходного для получения указателя ptr типа shared_ptr<T> .
Для указателя shared_ptr определены следующие методы:
- get() – возвращает стандартный указатель на основе указателя shared_ptr без передачи прав на объект. После вызова метода указатель shared_ptr указывает на тот же объект, что и до вызова;
- swap() – обменивает местами значения двух указателей;
- reset() – сбрасывает права владения на объект;
- use_count() – возвращает количество копий указателей на объект. Учитываются также стандартные указатели, полученные методом get() ;
- операторная функция operator=() реализующая присваивание указателей типа shared_ptr .
2.3. Указатель weak_ptr
Указатель weak_ptr реализован в пространстве имен std . Соответственно, обращение к указателю имеет вид std::weak_ptr .
Указатель std::weak_ptr – это умный указатель, содержащий так называемую «слабую» ссылку на объект на который указывает указатель типа shared_ptr . Термин «слабый» означает, что указатель weak_ptr не является владельцем объекта, на который он указывает. Владелец этого объекта — указатель shared_ptr .
Указатель weak_ptr определяет временное владение – это случай, когда к объекту нужно получить доступ только тогда, когда на него уже указывает указатель shared_ptr , имеющий право владения на этот объект. При этом изменять значение объекта с помощью weak_ptr не получится. Значение объекта можно только считывать.Чтобы с помощью указателя weak_ptr можно было изменить значение объекта на который он указывает, нужно его конвертировать в указатель shared_ptr с помощью метода lock() . В этом случае, если исходный указатель shared_ptr уничтожается, то время жизни объекта продолжается до тех пор, пока не будет уничтожен временный shared_ptr , который был получен из weak_ptr .
Указатель weak_ptr полезен в случае, когда нужно обрабатывать несколько ссылок, циклически указывающих на объекты управляемые указателями shared_ptr . Если в таком цикле ссылок нет общих внешних указателей, то счетчик shared_ptr не может достичь нуля. В результате происходит утечка памяти (memory leak). Чтобы избежать этой ситуации, один из указателей в цикле устанавливается как weak_ptr .В общем случае получение указателя типа weak_ptr из указателя типа shared_ptr выглядит следующим образом
Умные указатели в современном C++ с точки зрения новичка
Указатели в языках C и C++ — те еще штучки. Они чрезвычайно мощные, но в то же время такие опасные: достаточно небольшого недосмотра, чтобы сломать все ваше приложение. Проблема в том, что управление указателями полностью зависит от вас. За каждым динамическим выделением объекта (например, new T) должно следовать ручное удаление (например, delete T). Забудете это сделать, и в итоге получите хорошенькую утечку памяти.
Более того, динамически выделяемые массивы (например, new T[N]) необходимо удалять с помощью другого оператора (например, delete[]). Поэтому приходится мысленно отслеживать, что вы выделили, и соответственно вызывать нужный оператор. Ошибки с выбором формы приводят к неопределенному поведению, чего при работе на C++ нужно избегать любой ценой.
Другая деликатная проблема заключается во владении. Сторонняя функция возвращает указатель: а не динамически ли были выделены эти данные? Если да, то кто отвечает за очистку? Невозможно получить такую информацию, просто посмотрев на возвращаемый тип.
Суть умных указателей
Умные указатели были созданы для устранения вышеупомянутых неудобств. По сути, они обеспечивают автоматическое управление памятью: когда умный указатель больше не используется, то есть выходит из области видимости, память, на которую он указывает, автоматически высвобождается. Традиционные указатели теперь также называют «обычными» указателями.
Мне нравится рассматривать умные указатели как упаковки, в которых хранятся динамические данные. На самом деле это просто классы, которые оборачивают обычный указатель в свои недра и перегружают операторы -> и *. Благодаря этому трюку умный указатель имеет тот же синтаксис, что и обычный указатель. Когда умный указатель выходит из области видимости, срабатывает его деструктор и происходит очистка памяти. Эта техника называется Resource Acquisition Is Initialization (RAII): класс оборачивает динамический ресурс (файл, сокет, подключение к базе данных, выделенная память, . ), который должным образом удаляется/закрывается в своем деструкторе. Таким образом, вы гарантированно избежите утечки ресурсов.
Умные указатели можно рассматривать как примитивную реализацию сборки мусора: своего рода автоматическое управление памятью, где объект автоматически удаляется, когда он больше не используется программой.
Бонусный момент: если сторонняя функция возвращает умный указатель, вы можете быстро определить его тип, что с ним можно делать и как управляется время жизни данных.
Типы умных указателей в современном C++
- std::unique_ptr — умный указатель, владеющий динамически выделенным ресурсом;
- std::shared_ptr — умный указатель, владеющий разделяемым динамически выделенным ресурсом. Несколько std::shared_ptr могут владеть одним и тем же ресурсом, и внутренний счетчик ведет их учет;
- std::weak_ptr — подобен std::shared_ptr, но не увеличивает счетчик.
Сейчас все выглядит запутанно, особенно выбор того, какой тип умного указателя следует использовать. Не волнуйтесь, я расскажу обо всем в следующих параграфах и в следующем эпизоде. Давайте копнем глубже!
Понимание std::unique_ptr: одиночный вариант
std::unique_ptr владеет объектом, на который он указывает, и никакие другие умные указатели не могут на него указывать. Когда std::unique_ptr выходит из области видимости, объект удаляется. Это полезно, когда вы работаете с временным, динамически выделенным ресурсом, который может быть уничтожен после выхода из области действия.
Как создать std::unique_ptr
A std::unique_ptr создается следующим образом:
Также можно создать std::unique_ptrs с помощью специальной функции std::make_unique, вот так:
Если есть возможность, всегда старайтесь выделять объекты с помощью std::make_unique. Почему лучше поступать именно так, я покажу в последнем разделе этой статьи.
std::unique_ptr в действии
Главная особенность этого умного указателя — исчезать, когда он больше не используется. Рассмотрим следующий код:
Умный указатель выходит из области видимости, когда функция compute() достигает конца тела. Вызывается деструктор указателя, и память очищается автоматически. Больше ни о чем волноваться не нужно.
Один ресурс, один std::unique_ptr
Я могу сказать, что std::unique_ptr очень ревниво относится к динамическому объекту, который он хранит: невозможно иметь несколько ссылок на его динамические данные. Например:
Это сделано специально, и это важная особенность std::unique_ptr: на любой ресурс может указывать не более одного std::unique_ptr. Это предотвращает ошибочное многократное удаление указателя.
Технически это происходит потому, что у std::unique_ptr нет конструктора копирования: это может быть очевидно для вас, если вы знакомы с семантикой перемещения (я написал об этом вводную статью, если вы не знакомы). Во второй части этой статьи я покажу, как правильно передавать умные указатели.
Понимание std::shared_ptr: конвивиальный вариант
std::shared_ptr владеет объектом, на который он указывает, но, в отличие от std::unique_ptr, он допускает множественные ссылки. Специальный внутренний счетчик уменьшается каждый раз, когда std::shared_ptr, указывающий на тот же ресурс, выходит из области видимости. Эта техника называется подсчетом ссылок. Когда последняя из них будет уничтожена, счетчик станет равным нулю, и данные будут высвобождены.
Умный указатель такого типа полезен, когда требуется обмениваться динамически распределенными данными, точно так же, как это делается с обычными указателями или ссылками.
Существует альтернативный способ создания std::shared_ptr, использующий специальную функциюstd::make_shared:
Это должен быть наиболее предпочтительный способ построения такого рода умных указателей. Я покажу вам почему в последнем разделе этой статьи.
Проблемы с массивами
До C++17 не было простого способа соорудить std::shared_ptr, хранящий массив. До C++17 этот умный указатель по умолчанию всегда вызывает delete (а не delete[]) на своем ресурсе: вы можете создать обходной путь, используя кастомное удаление. Один из многих конструкторов std::shared_ptr принимает в качестве второго параметра лямбду, в которой вы вручную удаляете принадлежащий ему объект. Например:
К сожалению, нет возможности сделать это при использовании std::make_shared.
std::shared_ptr в действии
Одна из главных особенностей std::shared_ptr — возможность отслеживать, сколько указателей ссылаются на один и тот же ресурс. Получить информацию о количестве ссылок можно с помощью метода use_count(). Рассмотрим следующее:
Обратите внимание, как ptr и ptr_copy выходят из области видимости в конце функции, доводя счетчик ссылок до нуля. В этот момент деструктор последнего объекта обнаруживает, что ссылок больше нет, и запускает очистку памяти.
Один ресурс, много std::shared_ptr. Не забывайте о циклических ссылках!
Сила множественных ссылок может привести к неприятным сюрпризам. Скажем, я пишу игру, в которой у игрока есть другой игрок в качестве компаньона, например, так:
Логично, не так ли? К сожалению, я только что создал так называемую круговую ссылку. В начале моей программы я создаю два умных указателя jasmine и albert, которые хранят динамически создаваемые объекты: назовем эти динамические данные jasmine-data и albert-data, чтобы было понятнее.
Затем в (1) я передаю jasmine указатель на albert-data, а в (2) albert хранит указатель на jasmine-data. Это все равно что дать каждому игроку компаньона.
Когда jasmine выходит из области видимости в конце программы, ее деструктор не может очистить память: все еще есть один умный указатель, указывающий на jasmine-data, это albert->companion. Аналогично, когда albert выходит из области видимости в конце программы, его деструктор не может очистить память: ссылка на albert-data все еще живет через jasmine->companion. В этот момент программа просто завершается, не освободив память: утечка памяти во всем ее великолепии. Если вы запустите приведенный выше фрагмент, то заметите, что
Player() никогда не будет вызван.
Это не такая большая проблема, так как операционная система позаботится об очистке памяти за вас. Однако вам не стоит иметь такие круговые зависимости (т.е. утечки памяти) в середине вашей программы. К счастью, на помощь придет последний тип умного указателя.
Понимание std::weak_ptr: поверхностный вариант
std::weak_ptr — это, по сути, std::shared_ptr, который не увеличивает счетчик ссылок. Он определяется как умный указатель, который содержит несобственную ссылку, или ослабленную ссылку, на объект, управляемый другим std::shared_ptr.
Этот умный указатель полезен для решения некоторых раздражающих проблем, которые нельзя решить с помощью необработанных указателей. Вскоре мы увидим, как это сделать.
Как создать std::weak_ptr
Вы можете создать std::weak_ptr только из std::shared_ptr или другого std::weak_ptr. Например:
В приведенном выше примере p_weak1 и p_weak2 указывают на одни и те же динамические данные, принадлежащие p_shared, но счетчик ссылок не растет.
std::weak_ptr в действии
std::weak_ptr является своего рода инспектором дляstd::shared_ptr от которого он зависит. Вы должны сначала преобразовать его в std::shared_ptr с помощью метода lock() если вы действительно хотите работать с реальным объектом:
Конечно, p_shared_orig может быть нулевым в случае, если p_shared был удален в другом месте.
std::weak_ptr решает проблемы
С помощью std::weak_ptr очень легко решить проблему висящих указателей — тех, которые указывают на уже удаленные данные. Он предоставляет метод expired(), который проверяет, был ли объект, на который ссылается ссылка, уже удален. Если expired() == true, исходный объект был где-то удален, и вы можете действовать соответствующим образом. Это то, что вы не можете сделать с необработанными указателями.
Как я уже говорил, std::weak_ptr также используется для разрыва циклической ссылки. Давайте вернемся к примеру Player, приведенному выше, и изменим переменную-член с std::shared_ptr companion на std::weak_ptr companion. В данном случае мы использовали std::weak_ptr для устранения запутанного владения. Фактически имкющиеся динамически выделяемые данные остаются в основном теле, в то время как каждый Player теперь имеет слабую ссылку на них. Запустите код с этим изменением, и вы увидите, что деструктор вызывается дважды, правильно.
Заключительные заметки и мысли об умных указателях
В этой статье я хотел дать обзор различных типов умных указателей в C++ и описать их свойства. Давайте закончим этот обзор, высказав еще некоторые мысли.
Мне нравятся умные указатели. Должен ли я навсегда избавиться от new/delete?
- когда вам нужно кастомное удаление, как мы видели ранее, когда мы добавили поддержку массивов в std::shared_ptr;
- когда вы пишете собственные контейнеры и хотите вручную управлять памятью;
- с помощью так называемой конструкции in-place, более известной как placement new: новый способ создания объекта на уже выделенной памяти. Более подробная информация здесь.
Согласно различным источникам (здесь и здесь), производительность умных указателей должна быть близка к производительности необработанных указателей. Небольшое снижение скорости может присутствовать в std::shared_ptr из-за внутреннего подсчета ссылок. В целом, есть некоторые накладные расходы, но они не должны сделать код медленным, если только вы не будете постоянно создавать и уничтожать умные указатели.
Рациональное обоснование std::make_unique и std::make_shared
Этот альтернативный способ построения умных указателей дает два преимущества. Во-первых, он позволяет нам забыть о ключевом слове new. При работе с умными указателями мы хотим избавиться от гнусной комбинации new/delete, верно? Во-вторых, это делает ваш код защищенным от исключений. Рассмотрим вызов функции, принимающей на вход два умных указателя, следующим образом:
Предположим, что new A() выполняется успешно, но new B() выбрасывает исключение: вы ловите его, чтобы возобновить нормальное выполнение программы. К сожалению, стандарт C++ не требует, чтобы объект A был уничтожен, а его память высвобождена: память тихо утекает, и нет способа ее очистить. Обернув A и B в std::make_unique, вы будете уверены, что утечка не произойдет:
Дело в том, что std::make_unique<А> и std::make_unique<В> теперь являются временными объектами, а очистка временных объектов правильно указана в стандарте C++: их деструкторы будут вызваны и память освобождена. Поэтому, если есть возможность, всегда предпочитайте выделять объекты с помощью std::make_unique и std::make_shared.
What is a smart pointer and when should I use one?
This answer is rather old, and so describes what was ‘good’ at the time, which was smart pointers provided by the Boost library. Since C++11, the standard library has provided sufficient smart pointers types, and so you should favour the use of std::unique_ptr , std::shared_ptr and std::weak_ptr .
There was also std::auto_ptr . It was very much like a scoped pointer, except that it also had the «special» dangerous ability to be copied — which also unexpectedly transfers ownership.
It was deprecated in C++11 and removed in C++17, so you shouldn’t use it.OLD ANSWER
A smart pointer is a class that wraps a ‘raw’ (or ‘bare’) C++ pointer, to manage the lifetime of the object being pointed to. There is no single smart pointer type, but all of them try to abstract a raw pointer in a practical way.
Smart pointers should be preferred over raw pointers. If you feel you need to use pointers (first consider if you really do), you would normally want to use a smart pointer as this can alleviate many of the problems with raw pointers, mainly forgetting to delete the object and leaking memory.
With raw pointers, the programmer has to explicitly destroy the object when it is no longer useful.
A smart pointer by comparison defines a policy as to when the object is destroyed. You still have to create the object, but you no longer have to worry about destroying it.
The simplest policy in use involves the scope of the smart pointer wrapper object, such as implemented by boost::scoped_ptr or std::unique_ptr .
Note that std::unique_ptr instances cannot be copied. This prevents the pointer from being deleted multiple times (incorrectly). You can, however, pass references to it around to other functions you call.
std::unique_ptr s are useful when you want to tie the lifetime of the object to a particular block of code, or if you embedded it as member data inside another object, the lifetime of that other object. The object exists until the containing block of code is exited, or until the containing object is itself destroyed.
A more complex smart pointer policy involves reference counting the pointer. This does allow the pointer to be copied. When the last «reference» to the object is destroyed, the object is deleted. This policy is implemented by boost::shared_ptr and std::shared_ptr .
Reference counted pointers are very useful when the lifetime of your object is much more complicated, and is not tied directly to a particular section of code or to another object.
There is one drawback to reference counted pointers — the possibility of creating a dangling reference:
Another possibility is creating circular references:
To work around this problem, both Boost and C++11 have defined a weak_ptr to define a weak (uncounted) reference to a shared_ptr .