共享指针如何工作?

cam*_*cam 38 c++ shared-ptr c++11

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

Jam*_*lis 61

基本上,shared_ptr有两个指针:一个指针,指向共享对象的指针和指向含有两个引用计数一个结构:一个用于"强引用,"或具有所有权的引用,一个用于"弱引用,"或引用不拥有所有权.

复制a时shared_ptr,复制构造函数会增加强引用计数.当你破坏a时shared_ptr,析构函数会减少强引用计数并测试引用计数是否为零; 如果是,则析构函数删除共享对象,因为没有shared_ptrs指向它.

弱引用计数用于支持weak_ptr; 基本上,无论weak_ptr何时从a 创建shared_ptr,弱引用计数都会递增,并且任何时候一个被销毁,弱引用计数就会递减.只要强引用计数或弱引用计数大于零,引用计数结构就不会被销毁.

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

  • 而且`make_shared`的好主意是将两个项目(计数结构和对象)捆绑到一个内存块中,这样你只需要一个内存分配(因此更快)而不是两个......(好吧,还有一个但是不要紧).另外,精确地说,单独计数结构的想法是为了使`weak_ptr`能够在count结构的指针处通过`expired`函数>来知道对象是否被删除.为null,不再有对象指向. (4认同)
  • @MarcvanLeeuwen:当对计数和对象使用单个内存分配时,强计数决定何时调用析构函数,而两个计数决定何时释放内存块。“额外”的内存保留在实践中很少成为问题,因为 `weak_ptr` 一开始并不经常使用。如果这是特定情况下的问题,那么您确实可能希望对其使用单独的分配。 (2认同)

val*_*ldo 16

我普遍同意James McNellis的回答.但是还有一点需要提及.

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

那是:

class AbraCadabra;

boost::shared_ptr<AbraCadabra> myPtr;
// ...
Run Code Online (Sandbox Code Playgroud)

这将编译和工作.与智能指针的许多其他实现不同,智能指针实际上要求完全定义封装类型以便使用它们.这与智能指针在不再被引用时知道删除封装对象的事实有关,并且为了删除对象,必须知道它是什么.

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

  1. 指向对象的不透明指针
  2. 共享参考计数器(James McNellis描述的内容)
  3. 指向已分配工厂的指针,该工厂知道如何销毁对象.

上面的工厂是一个带有单个虚函数的辅助对象,它应该以正确的方式删除你的对象.

实际上,在为共享指针赋值时会创建此工厂.

也就是说,以下代码

AbraCadabra* pObj = /* get it from somewhere */;
myPtr.reset(pObj);
Run Code Online (Sandbox Code Playgroud)

这是该工厂的分配地点.注意:该reset函数实际上是一个模板函数.它实际上为指定的类型(作为参数传递的对象的类型)创建工厂.这是您的类型应该完全定义的地方.也就是说,如果它仍未定义 - 您将收到编译错误.

另请注意:如果您实际创建派生类型的对象(派生自AbraCadabra),并将其分配给shared_ptr- 它将以正确的方式删除,即使您的析构函数不是虚拟的.在shared_ptr根据是认为,在类型将始终删除对象reset的功能.

所以shared_ptr是一个非常复杂的智能指针变体.它提供了极好的灵活性.但是,您应该知道,与智能指针的其他可能实现相比,这种灵活性的代价是性能极差.

另一方面 - 有所谓的"侵入式"智能指针.它们并不具备所有的灵活性,但相比之下它们可以提供最佳性能.

shared_ptr与inrusive智能指针相比的优点:

  • 使用非常灵活.只需在将其分配给封装类型时定义封装类型shared_ptr.这对于大型项目非常有价值,可以大大减少依赖性.
  • 封装类型不必具有虚拟析构函数,仍将正确删除多态类型.
  • 可以与弱指针一起使用.

的利弊shared_ptr相比inrusive智能指针:

  1. 非常野蛮的性能和浪费堆内存.在赋值时分配另外两个对象:引用计数器,加上工厂(浪费内存,慢).然而,这只发生在reset.当一个shared_ptr被分配给另一个 - 没有更多的分配.
  2. 以上可能会抛出异常.(内存不足的情况).相反,intrsusive智能指针可能永远不会抛出(除了与无效内存访问,堆栈溢出等相关的进程异常)
  3. 删除对象也很慢:需要释放另外两个结构.
  4. 使用侵入式智能指针时,您可以自由地将智能指针与原始指针混合使用.这没关系,因为实际引用计数驻留在对象本身内部,这是单个的.相反 - shared_ptr你可能不会与原始指针混在一起.

    AbraCadabra*pObj =/*从某个地方获取它*/; myPtr.reset(pObj); // ... pObj = myPtr.get(); boost :: shared_ptr myPtr2(pObj); //哎呀

以上将崩溃.


MSa*_*ers 11

至少有三种众所周知的机制.

外部计数器

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

内部柜台

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

循环链接

您可以将所有共享指针保存在循环图中的对象中,而不是使用计数器.创建的第一个指针指向自身.复制指针时,将副本插入圆圈.删除后,将其从圆圈中删除.但是当被破坏的指针指向自身时,即当它是唯一的指针时,您将删除指向的对象.

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

变化

可以组合第二个和第三个想法:基类可以是该循环图的一部分,而不是包含计数.当然,这意味着只有当对象指向自身时才能删除它(循环长度为1,没有剩余的指针).同样,优点是你可以从弱指针创建智能指针,但是从链中删除指针的糟糕表现仍然是个问题.

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