为什么shared_ptr需要对weak_ptr进行引用计数?

11

引用自C++ Primer $12.1.6:

weak_ptr(表12.5)是一个智能指针,它不控制指向的对象的生命周期。相反,weak_ptr指向由shared_ptr管理的对象。将weak_ptr绑定到shared_ptr 不会改变该shared_ptr的引用计数。一旦指向该对象的最后一个shared_ptr消失,该对象本身将被删除。即使有weak_ptr指向它,该对象也将被删除,因此称为weak_ptr,它捕捉到一个“弱”地共享其对象的想法。

然而,我读过一篇文章说:

使用make_shared更加高效。shared_ptr的实现必须在所有引用给定对象的shared_ptr和weak_ptr之间共享控制块中维护管理信息。特别是,该管理信息不仅需要包括一个而是两个引用计数: 1. “强引用”计数以跟踪当前保持对象存活的shared_ptr数量。当最后一个强引用消失时,共享对象被销毁(并可能被释放)。 2. “弱引用”计数以跟踪当前观察对象的weak_ptr数量。当最后一个弱引用消失时,共享管理控制块被销毁和释放(如果尚未释放,则共享对象被释放)。
据我所知,由make_shared创建的shared_ptr与这些引用计数位于同一控制块中。因此,只有当最后一个weak_ptr过期时,对象才会被释放。
  1. Is the Primer wrong? Because weak_ptr will actually affects the lifetime of that object.
  2. Why does the shared_ptr need to track its weak refs?The weak_ptr can tell if the object exists by checking the strong refs in control blocks,so I think the control block does not need to track the weak refs.
  3. Just for curiosity,what does the control block created by shared_ptr look like?Is it something like:

    template<typename T>
    class control_block
    {
       T object;
       size_t strong_refs;
       size_t weak_refs;
       void incre();
       void decre();
       //other member functions...
    };
    //And in shared_ptr:
    template<typename T>
    class shared_ptr
    {
       control_block<T> block;//Is it like this?So that the object and refs are in the same block?
       //member functions...
    };
    

1
@juanchopanza 这就是为什么我感到困惑,正如第二段引述的那样,该对象位于控制块中,并且直到最后一个弱引用过期才会被释放。 - choxsword
3
这里有两个对象需要管理:指向对象,当最后一个 shared_ptr 被移除时将被删除;以及 控制块对象,它包含了引用计数,当最后一个指向它的 managed 指针被删除时(包括 shared 和 weak ptrs),该对象也会被删除。 - PeterT
1
销毁和释放是两个不同的事件。在使用make_shared的情况下,仍然存在一个指针,只有1次较少的分配。 - rustyx
1
@bigxiao 你可以直接调用析构函数。 - llllllllll
1
@bigxiao:更准确地说,delete ptr通过调用指向对象的析构函数来销毁该对象,如果有的话,这将结束对象的生命周期,然后调用operator delete(),它可以针对对象类型进行特定处理,默认情况下,它会释放为对象分配的堆内存。 - Maarten Hilferink
显示剩余4条评论
4个回答

24

引用计数控制指向对象的生命周期。弱引用计数不会控制指向对象的生命周期,但会控制(或参与控制)控制块的生命周期。

如果引用计数降至0,对象将被销毁,但不一定释放内存。当弱引用计数降至0时(或者当引用计数降至0且此时没有weak_ptr存在),控制块将被销毁并释放内存,如果尚未释放,则对象的存储空间也将被释放。

销毁和释放指向对象的空间的分离是一种实现细节,您无需关心,但是这是使用make_shared导致的。

如果您这样做

shared_ptr<int> myPtr(new int{10});

你为 int 分配存储空间,然后将其传递到 shared_ptr 构造函数中,该函数单独为控制块分配存储空间。在这种情况下,int 的存储空间可以尽早被释放:一旦引用计数达到 0,即使仍存在弱引用计数。

如果你这样做:

auto myPtr = make_shared<int>(10);

那么make_shared可能会进行优化,其中它一次性分配int和控制块的存储空间。这意味着在控制块的存储空间可以被释放之前,int的存储空间不能被释放。当引用计数达到0时,int的生命周期结束,但直到弱引用计数达到0时才会释放其存储空间。

现在清楚了吗?


控制块为什么需要跟踪weak_refs?如果shared_ptr是由new创建的,我认为在没有weak_refs时不需要释放控制块,因为控制块不占用太多空间。 - choxsword
1
我不明白控制块的大小和是否需要释放之间的联系?如果你没有在代码中引用它,而不进行释放,那就会造成内存泄漏。小并不代表它不会泄漏。 - BoBTFish
1
Primer使用的“deleted”一词具有误导性,这使我想到了delete ptr会释放内存的情况。 - choxsword
1
如果你忽略掉 make_shared 允许使用的优化,Primer 所说的完全正确。也就是说,如果你假设对象和控制块的存储是完全分离的,那么对象将会在同一时间被销毁并释放它的存储空间。我猜作者们决定你不需要知道这种优化的细节——毕竟这只是一个入门指南。 - BoBTFish
@bigxiao《C++ Primer》似乎混淆了两种不同的用法:shared_ptr<T> p(new T)make_shared<T>()。需要调用delete ptr,其中ptr是在构造p时使用的参数。(这实际上可以通过替换删除运算符来观察到。)当使用make_shared而不是new时,内存分配受该函数控制。 - curiousguy

10

weak_ptr需要指向一个能够告诉它对象是否存在的东西,以便知道是否可以转换为shared_ptr。因此需要一个小对象来管理这些信息。

这个管理控制块在最后一个weak_ptr(或shared_ptr)被删除时需要被销毁。因此,它必须计算shared_ptr和weak_ptr的数量。

请注意,管理控制块与指针所指向的对象不同,因此weak_ptr不会影响对象的生命周期。

实现智能指针有许多不同的方法,具体取决于您想要的行为。如果想了解更多信息,建议阅读Alexandrescu的《现代C++设计》(https://www.amazon.com/Modern-Design-Generic-Programming-Patterns/dp/0201704315)。


但是weak_ptr可以通过检查控制块中的强引用来判断对象是否存在,控制块不需要跟踪弱引用。 - choxsword
3
如果weak_ptr没有被追踪,那么控制块需要被删除,然后最后一个shared_ptr会被移除。此时weak_ptr将会指向一个不再存在的东西(即控制块),这是不好的。请注意,此处为直译,可能需要根据上下文进行调整以更符合实际情况或语境。 - madlers
如果shared_ptr是通过new创建的,我认为在没有弱引用时无需释放控制块,因为控制块不占用太多空间。 - choxsword
现在我完全不在你身边... 你是在暗示控制块存在内存泄漏吗? - madlers
2
@bigxiao 因为对象小就泄露,这并不可取。控制块已经被分配了,所以必须回收。如果泄漏大量小对象或少量大对象都可能导致内存耗尽。 - Miles Budnek
谢谢,我搞混了一些东西。 - choxsword

3

weak_ptr和shared_ptr都指向包含控制块的内存。如果你在shared_ptr计数器达到0时立即删除控制块(但weak计数器没有达到0),那么你会得到指向垃圾内存的weak_ptrs。然后,当你尝试使用weak_ptr时,它会读取已释放的内存,产生不良后果(UB)。

因此,只要有任何weak_ptr可能尝试读取它,控制块就必须保持活动状态(分配和构建,而不是销毁或释放)。

主(被指向的)对象将在shared计数器达到0时被销毁,并且可以(希望)立即被释放。当两个计数器都达到0时,控制块将被销毁和释放。


0

一个不错的第一步是在你对概念的心理表征中明确“销毁”和“释放内存”的区别,这也是比起“这是一个实现细节,你不需要关心它”更可取的步骤(善意地提到),因为后者会加深你的无知。

所以,让 SeriousObject 成为一个 class,其大小约为系统内存的一半,并在构造时控制鼠标。我们考虑在此情况下,一个被销毁但未释放内存实例所暗示的副作用。在这种情况下,尽管鼠标控制已经恢复,但你仍然只有一半的可用内存。最糟糕的情况是,在代码的某个地方存在一个被遗忘或甚至泄漏的weak_ptr,导致剩余的50%内存变得无用,这将成为整个执行过程的拖累。但是,至少它没有泄漏,对吧?

假设在这里结束我的推论,以下是我对你的每个问题的假设:

  1. 虽然我并不确切地知道,但我敢大胆打赌,有一半的钱用于作者在发表之前已经对文本进行了仔细检查,另一半则用于错误本身是否存在,如果存在,那么肯定已经被发现了。
  2. 因为在这种情况下,如果weak_ptr没有被跟踪,shared_ptr和控制块都已经被销毁,并且至少有一个指向objectweak_ptr存在,当weak_ptr尝试像你所建议的那样,在控制块中检查strong refs 的时候,你认为会发生什么?头疼得要命。
  3. 这一次,我可以明确地说,你、我以及许多其他与编码相关的人都忘记了这个谦卑的资源。这是一个既免费、开源、真实案例、具有行业水准质量的知识资源。前进吧,兄弟们!时机已经到来,我们应该抓住我们出生时赋予我们的权利,通过荣誉牢牢捆绑我们的特权,履行我们作为生命和超越死亡的责任!

PS(实际上更像是顺便提一下)

我个人感到困惑的不是 weak_ptr 的计数,而是在对象生命周期的特定阶段做出这样的优化决策,我指的是 一次性分配类型优化,并且详细解释一下,我的意思是选择优化 最短可能的仅出现一次 的生命周期阶段,而接受以技术和行为副作用为代价,并换取以他们辛勤劳动的成果为代价得到一把山羊粪作为收获。Pff


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接