Как общие указатели работают?

Как общие указатели знают, сколько указателей указывает на тот объект? (shared_ptr, в этом случае)

38
задан Constructor 14 August 2014 в 21:53
поделиться

5 ответов

По сути, shared_ptr имеет два указателя: указатель на разделяемый объект и указатель на структуру, содержащую два счетчика ссылок: один для "сильных ссылок", или ссылок, имеющих право собственности, и один для "слабых ссылок", или ссылок, не имеющих права собственности.

Когда вы копируете shared_ptr, конструктор копии увеличивает счетчик сильных ссылок. Когда вы уничтожаете shared_ptr, деструктор уменьшает счетчик сильных ссылок и проверяет, равно ли счетчик ссылок нулю; если да, деструктор удаляет разделяемый объект, поскольку на него больше не указывают shared_ptr.

Счетчик слабых ссылок используется для поддержки weak_ptr; в основном, каждый раз, когда weak_ptr создается из shared_ptr, счетчик слабых ссылок увеличивается, а каждый раз, когда один из них уничтожается, счетчик слабых ссылок уменьшается. До тех пор, пока либо счетчик сильных ссылок, либо счетчик слабых ссылок больше нуля, структура счетчика ссылок не будет уничтожена.

Эффективно, пока счетчик сильных ссылок больше нуля, разделяемый объект не будет удален. Пока счетчик сильных ссылок или счетчик слабых ссылок не равен нулю, структура счетчика ссылок не будет удалена.

61
ответ дан 27 November 2019 в 03:21
поделиться
​​

Существует по крайней мере три хорошо известных механизма.

Внешние счетчики

Когда создается первый общий указатель на объект, создается отдельный объект счетчика ссылок и инициализируется значением 1. Когда указатель копируется, счетчик ссылок увеличивается; когда указатель уничтожен, он уменьшается. Назначение указателя увеличивает одно значение и уменьшает другое (в этом порядке, иначе самоназначение ptr = ptr нарушится). Если счетчик ссылок достигает нуля, указатели больше не существуют и объект удаляется.

Внутренние счетчики

Внутренний счетчик требует, чтобы объект, на который указывает, имел поле счетчика. Обычно это достигается путем наследования от определенного базового класса. Взамен это экономит выделение счетчика ссылок в куче и позволяет многократно создавать общие указатели из необработанных указателей (с внешними счетчиками вы получите два счетчика для одного объекта)

Круговые ссылки

Вместо этого Используя счетчик, вы можете сохранить все общие указатели на объект в круговой диаграмме. Первый созданный указатель указывает на себя. Когда вы копируете указатель, вы вставляете копию в круг. Когда вы его удаляете, вы удаляете его из круга. Но когда уничтоженный указатель указывал на себя, то есть когда это единственный указатель, вы удаляете указанный объект.

Обратной стороной является то, что удаление узла из кольцевого односвязного списка довольно дорого, так как вам придется перебирать все узлы, чтобы найти предшественника. Это может быть особенно болезненно из-за плохой локализации ссылки.

Варианты

2-я и 3-я идеи могут быть объединены: базовый класс может быть частью этого кругового графа, а не содержать счетчик. Конечно, это означает, что объект можно удалить только тогда, когда он указывает на себя (длина цикла 1, на него нет оставшихся указателей). Опять же, преимущество состоит в том, что вы можете создавать интеллектуальные указатели из слабых указателей, но низкая производительность удаления указателя из цепочки остается проблемой.

Точная структура графа для идеи 3 не имеет большого значения. Вы также можете создать двоичную древовидную структуру с указанным объектом в корне. Опять же, сложная операция - удалить узел общего указателя из этого графа. Преимущество состоит в том, что если у вас много указателей на многих потоках, увеличение части графа не является высококонкурентной операцией.

11
ответ дан 27 November 2019 в 03:21
поделиться

Они содержат счетчик внутренних ссылок, который увеличивается в конструкторе копирования / присваивании shared_ptr и уменьшается в деструкторе. Когда счетчик достигает нуля, удерживаемый указатель удаляется.

Вот документация библиотеки Boost для интеллектуальных указателей. Я думаю, что реализация TR1 в основном такая же, как boost :: shared_ptr .

0
ответ дан 27 November 2019 в 03:21
поделиться

"Общий указатель - это умный указатель (объект C ++ с перегруженным оператором * () и оператором -> ()), который сохраняет указатель на объект и указатель на общий счетчик ссылок. Каждый раз, когда создается копия интеллектуального указателя с помощью конструктора копирования, счетчик ссылок увеличивается. Когда общий указатель уничтожается, счетчик ссылок для его объекта уменьшается. Общие указатели, построенные из необработанных указатели изначально имеют счетчик ссылок 1. Когда счетчик ссылок достигает 0, указанный объект уничтожается, а занимаемая им память освобождается. Вам не нужно явно уничтожать объекты: это будет сделано автоматически при запуске деструктора последнего указателя. . " От здесь .

0
ответ дан 27 November 2019 в 03:21
поделиться

В целом я согласен с ответом Джеймса Макнеллиса. Однако есть еще один момент, который следует упомянуть.

Как вы можете знать, shared_ptr может также использоваться, когда тип T не полностью определен.

То есть:

class AbraCadabra;

boost::shared_ptr<AbraCadabra> myPtr;
// ...

Это будет компилироваться и работать. В отличие от многих других реализаций умных указателей, которые фактически требуют, чтобы инкапсулированный тип был полностью определен для их использования. Это связано с тем, что умный указатель должен знать, что нужно удалить инкапсулированный объект, когда на него больше не ссылаются, а чтобы удалить объект, нужно знать, что он собой представляет.

Это достигается следующим приемом: shared_ptr фактически состоит из следующего:

  1. Непрозрачный указатель на объект
  2. Счетчики общих ссылок (то, что описал Джеймс Макнеллис)
  3. Указатель на выделенную фабрику, которая знает, как уничтожить ваш объект.

Указанная фабрика - это вспомогательный объект с единственной виртуальной функцией, которая должна корректно удалять ваш объект.

Эта фабрика фактически создается, когда вы присваиваете значение вашему общему указателю.

То есть, в следующем коде

AbraCadabra* pObj = /* get it from somewhere */;
myPtr.reset(pObj);

эта фабрика выделяется. Примечание: функция reset на самом деле является шаблонной функцией. Она фактически создает фабрику для указанного типа (типа объекта, переданного в качестве параметра). Именно здесь ваш тип должен быть полностью определен. То есть, если он все еще не определен - вы получите ошибку компиляции.

Заметим также: если вы действительно создадите объект производного типа (производный от AbraCadabra) и присвоите его shared_ptr - он будет удален корректным образом, даже если ваш деструктор не виртуальный. shared_ptr всегда будет удалять объект в соответствии с типом, который видит функция reset.

Таким образом, shared_ptr - это довольно сложный вариант умного указателя. Он дает потрясающую гибкость. Однако вы должны знать, что за эту гибкость приходится платить ценой крайне низкой производительности по сравнению с другими возможными реализациями умного указателя.

С другой стороны - существуют так называемые "навязчивые" умные указатели. Они не обладают такой гибкостью, однако, в отличие от них, дают наилучшую производительность.

Плюсы shared_ptr по сравнению с интрузивными смарт-указателями:

  • Очень гибкое использование. Нужно только определить инкапсулированный тип при присвоении его shared_ptr. Это очень ценно для больших проектов, значительно уменьшает количество зависимостей.
  • Инкапсулированный тип не обязательно должен иметь виртуальный деструктор, полиморфные типы будут удаляться корректно.
  • Можно использовать со слабыми указателями.

Минусы shared_ptr по сравнению с инкрустированными умными указателями:

  1. Очень варварская производительность и трата памяти кучи. При присвоении выделяет еще 2 объекта: счетчики ссылок, плюс фабрику (трата памяти, медленно). Однако это происходит только при сбросе. Когда один shared_ptr присваивается другому - больше ничего не выделяется.
  2. Вышеописанное может вызвать исключение. (состояние вне памяти). В отличие от этого интрузивные смарт-указатели никогда не могут выбросить исключение (кроме исключений процесса, связанных с недопустимым доступом к памяти, переполнением стека и т.д.)
  3. Удаление вашего объекта также происходит медленно: нужно деаллоцировать еще две структуры.
  4. При работе с навязчивыми смарт-указателями вы можете свободно смешивать смарт-указатели с сырыми. Это нормально, потому что фактический подсчет ссылок находится внутри самого объекта, который является единым. Напротив - при работе с shared_ptr вы можете не смешивать с raw указателями.

    AbraCadabra* pObj = /* взять откуда-то */; myPtr.reset(pObj); // ... pObj = myPtr.get(); boost::shared_ptr myPtr2(pObj); // oops

Вышеприведенное приведет к краху.

16
ответ дан 27 November 2019 в 03:21
поделиться
Другие вопросы по тегам:

Похожие вопросы: