46

共享指针如何知道有多少指针指向该对象?(shared_ptr,在这种情况下)

4

5 回答 5

76

基本上,shared_ptr有两个指针:一个指向共享对象的指针和一个指向包含两个引用计数的结构的指针:一个用于“强引用”或拥有所有权的引用,一个用于“弱引用”或没有所有权的引用拥有所有权。

当您复制 ashared_ptr时,复制构造函数会增加强引用计数。当你销毁 ashared_ptr时,析构函数会递减强引用计数并测试引用计数是否为零;如果是,析构函数将删除共享对象,因为不再有shared_ptrs 指向它。

弱引用计数用于支持weak_ptr;基本上,任何时候weak_ptr从 中创建a shared_ptr,弱引用计数都会增加,而任何时候破坏弱引用计数都会减少。只要强引用计数或弱引用计数大于零,引用计数结构就不会被破坏。

实际上,只要强引用计数大于零,共享对象就不会被删除。只要强引用计数或弱引用计数不为零,引用计数结构体就不会被删除。

于 2010-05-10T13:36:44.223 回答
19

我大体上同意 James McNellis 的回答。然而,还有一点需要提及。

您可能知道,shared_ptr<T>也可以在类型T未完全定义时使用。

那是:

class AbraCadabra;

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

这将编译和工作。与许多其他智能指针实现不同,后者实际上需要完全定义封装类型才能使用它们。这与智能指针应该知道在不再引用它时删除封装对象这一事实有关,并且为了删除一个对象,必须知道它是什么。

这是通过以下技巧实现的:shared_ptr实际上包括以下内容:

  1. 指向对象的不透明指针
  2. 共享引用计数器(James McNellis 所描述的)
  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 个对象:引用计数器,加上工厂(浪费内存,速度慢)。然而,这仅发生在reset. 当一个shared_ptr分配给另一个时 - 没有更多的分配。
  2. 以上可能会抛出异常。(内存不足的情况)。相反,侵入式智能指针可能永远不会抛出(除了与无效内存访问、堆栈溢出等相关的进程异常)
  3. 删除对象也很慢:需要释放另外两个结构。
  4. 使用侵入式智能指针时,您可以自由地将智能指针与原始指针混合。这没关系,因为实际的引用计数驻留在对象本身内部,它是单一的。相比之下 -shared_ptr你可能不会与原始指针混合。
    AbraCadabra* pObj = /* get it from somewhere */;
    myPtr.reset(pObj);
    // ...
    pObj = myPtr.get();
    boost::shared_ptr<AbraCadabra> myPtr2(pObj); // oops

以上会崩溃。

于 2010-05-10T15:30:04.940 回答
12

至少存在三种众所周知的机制。

外部计数器

当创建第一个指向对象的共享指针时,会创建一个单独的引用计数对象并将其初始化为 1。当指针被复制时,引用计数会增加;当指针被销​​毁时,它会减少。指针分配增加一个计数并减少另一个计数(按此顺序,否则自分配ptr=ptr将中断)。如果引用计数为零,则不再存在指针并删除对象。

内部计数器

内部计数器要求指向的对象具有计数器字段。这通常是通过从特定的基类派生来实现的。作为交换,这节省了引用计数的堆分配,并允许从原始指针重复创建共享指针(使用外部计数器,您最终会为一个对象获得两个计数)

循环链接

您可以将所有指向对象的共享指针保存在圆形图中,而不是使用计数器。创建的第一个指针指向自身。复制指针时,会将副本插入圆圈中。当您删除它时,您将其从圈子中删除。但是当被销毁的指针指向它自己时,即当它是唯一的指针时,你就删除了指向的对象。

缺点是从循环单链表中删除一个节点相当昂贵,因为您必须遍历所有节点才能找到前一个节点。由于参考的局部性差,这可能会特别痛苦。

变化

第二个和第三个想法可以结合起来:基类可以是那个圆形图的一部分,而不是包含一个计数。当然,这意味着对象只有在指向自身时才能被删除(循环长度为 1,没有剩余的指向它的指针)。同样,优点是您可以从弱指针创建智能指针,但是从链中删除指针的性能不佳仍然是一个问题。

想法 3 的确切图形结构并不重要。您还可以创建一个二叉树结构,指向的对象位于根部。同样,硬操作是从该图中删除一个共享指针节点。好处是,如果您在许多线程上有许多指针,则图的增长部分不是一个高度竞争的操作。

于 2010-05-10T14:34:26.907 回答
0

它们拥有一个内部引用计数,该计数在 shared_ptr 复制构造函数/赋值运算符中递增,在析构函数中递减。当计数达到零时,删除保持的指针。

这是智能指针的 Boost 库文档。我认为 TR1 的实现与boost::shared_ptr.

于 2010-05-10T13:35:11.633 回答
0

“共享指针是一个智能指针(一个重载 operator*() 和 operator->() 的 C++ 对象),它保存一个指向对象的指针和一个指向共享引用计数的指针。每次制作智能指针的副本使用复制构造函数,引用计数增加。当共享指针被销毁时,其对象的引用计数减少。从原始指针构造的共享指针最初的引用计数为 1。当引用计数达到 0 时,指向的对象被销毁,它占用的内存被释放。你不需要显式地销毁对象:它会在最后一个指针的析构函数运行时自动完成。“从这里

于 2010-05-10T13:35:25.217 回答