【问题标题】:How does a reference-counting smart pointer's reference counting work?引用计数智能指针引用计数如何工作?
【发布时间】:2010-10-18 00:43:52
【问题描述】:

换句话说,实现如何跟踪计数?

是否维护了一个类似地图的对象,所有shared_ptr 实例都可以访问该对象,其键是指针的地址,值是引用的数量?如果我必须实现shared_ptr,这是我想到的第一个想法。

如果这些引用计数智能指针存在内存泄漏的可能性?如果是这样,我该如何避免它们?

【问题讨论】:

    标签: c++ memory-management memory-leaks smart-pointers shared-ptr


    【解决方案1】:

    没有。 shared_ptr 只保留一个额外的指针用于引用计数。

    当您复制 shared_ptr 对象时,它会复制带有引用计数的指针,增加它,并在包含的对象上复制指针。

    【讨论】:

      【解决方案2】:

      使用引用计数智能指针创建内存泄漏非常容易。只需创建在图中具有循环的对象的任何类似图形的结构。循环中的对象会互相阻止释放。这无法自动解决 - 例如,当您创建一个双链接列表时,您必须注意不要一次删除多个对象。

      【讨论】:

        【解决方案3】:

        每个智能指针对象都包含一个共享引用计数 - 每个原始指针对应一个。

        您可以查看this 文章。此实现将这些存储在一个单独的对象中,该对象被复制。您还可以查看boost's documentation 或查看智能指针上的Wikipedia article

        【讨论】:

        • -1。引用同一对象的所有智能指针必须共享​​>一个引用计数。持有引用计数的对象由第一个智能指针对象分配,指向它的指针(不是对象本身)在智能指针被复制时被复制。
        • 同意 j_random_hacker。每个原始指针的计数都是唯一的,并且由引用相同原始指针的所有 shared_ptr 共享。通常它被分配为一个单独的内存块,因此 smart_ptr 保存两个内部 ptr,一个用于引用计数,另一个用于指针本身。
        • -1 用于静态变量。除非你正在实现一个指向单例对象的引用计数智能指针,否则你不能使用静态来实现引用计数。
        • 我想我的措辞搞砸了,我的意思是每个指针都有一个引用计数并且每个智能指针都引用它。修复它(我希望)
        • 我认为也许 Ferruccio 的回答会成为一个更好接受的答案
        【解决方案4】:

        据我记忆,在Effective C++的一章里有一个引用计数指针的问题。

        原则上,您有一个“轻量级”指针类,其中包含一个指向持有引用的类的指针,该引用知道递增/递减引用并销毁指针对象。该引用计数类指向要引用的对象。

        【讨论】:

          【解决方案5】:

          我已经看到了两种不同的非侵入式方法:

          1. 智能指针分配一个小的 包含的内存块 参考计数器。每个副本 智能指针然后接收一个 指向实际对象的指针和 指向引用计数的指针。
          2. 除了一个对象指针, 每个智能指针包含一个 上一个和下一个指针,从而 形成一个双向链表 指向特定的智能指针 目的。参考计数是 隐含在列表中。当一个聪明人 指针被复制,它将自身添加到 名单。破坏后,每个 智能指针从 名单。如果是最后一个 然后它释放的列表 引用的对象也是如此。

          如果你去here 并滚动到底部,有一个很好的图表可以更清楚地解释这些方法。

          【讨论】:

          • 链表方法避免了额外的分配,但是如果没有全局互斥体,很难做到“线程安全”。 (“线程安全”与“作为原始指针一样线程安全”)
          • 另外,如果你使用make_shared,它也可以通过将分配的对象和实例计数器放在一个内存块中来避免额外的分配。
          • 不知道链表方法。与我熟悉的第一种方法相比,我可以看到它的一些优点。即没有多余的“小块内存”,也不需要担心算术溢出。
          • @Assimilater - 我认为链表方法没有任何真正的优势。您必须维护该列表,这对线程安全和性能有影响。而如果你使用make_shared 并且每个人应该无论如何都使用make_shared,额外的块分配通常会消失。此外,如果在这种情况下算术溢出确实存在,我怀疑代码库存在更严重的问题。
          • @Assimilater - 你分配了一个足够大的块来容纳对象和引用计数。然后将引用计数存储在对象的最后一个字节之后。
          【解决方案6】:

          许多答案解决了引用计数的存储方式(它存储在共享内存中,供所有持有相同本机指针的 shared_ptr 使用),但大多数都避免了泄漏问题。

          使用引用计数指针泄漏内存的最简单方法是创建循环。例如,所有指针都是 shared_ptr 且至少有两个元素的双向链表保证不会被删除。即使外部指针被释放,内部指针仍然会计数,并且引用计数不会达到 0。至少,在最幼稚的实现中是这样。

          循环问题最简单的解决方案是将shared_ptr(引用计数指针)与不共享对象所有权的弱指针混合。

          共享指针将共享资源(指针)和附加的reference_count 信息。当你使用弱指针时,引用计数加倍:有一个共享指针引用计数和一个弱指针引用计数。每当共享指针计数达到 0 时,资源就会被释放,但 reference_count 信息会一直保持活动状态,直到最后一个弱指针被释放。

          在双向链表中,外部引用保存在一个shared_ptr中,而内部链接只是weak_ptr。只要没有外部引用(shared_ptr),列表的元素就会被释放,删除弱引用。最后,所有弱引用都被删除,每个资源的最后一个弱指针释放了reference_count信息。

          它不像上面的文字看起来那么令人困惑......我稍后再试一次。

          【讨论】:

          • 您没有稍后再试。
          【解决方案7】:

          实现 RC 的类基本上保持对它所管理的内存地址的引用数(来自类的其他对象,包括它自己的)的计数。只有当对内存地址的引用计数为零时才会释放内存。

          让我们看一些代码:

          template <class T>
          class SharedPtr
          {
              T* m_ptr;   
              unsigned int* r_count;  
          public:
              //Default Constructor
              SharedPtr(T* ptr) :m_ptr{ ptr }, r_count{ ptr ? new unsigned int : nullptr }
              {
                  if (r_count)
                  {
                      *r_count = 1;
                  }
              }
          
              //Copy Constructor
              SharedPtr(SharedPtr& ptr) :m_ptr{ ptr.m_ptr }, r_count{ ptr.m_ptr ? new unsigned int : nullptr }
              {
                  if (ptr.r_count)
                  {
                      ++(*ptr.r_count);
                      r_count = ptr.r_count;
                      m_ptr = ptr.m_ptr;
                  }
              }
          
              //Copy Assignment
              SharedPtr& operator=(SharedPtr& ptr)
              {
                  if (&ptr == this)
                      return *this;
                  if (ptr.r_count)
                  {
                      delete m_ptr;
                      ++(*ptr.r_count);
                      r_count = ptr.r_count;
                      m_ptr = ptr.m_ptr;
                  }
                  return *this;
              }
          
              //Destructor
              ~SharedPtr()
              {
                  if (r_count)
                  {
                      --(*r_count);
                      if (!(*r_count))
                      {
                          delete m_ptr;
                          delete r_count;
                      }
                  }
              }
          };
          

          以下是上述SharedPtr 类如何工作的详细信息:

          内部变量

          内部指针 m_ptr

          SharedPtr 类的指针,它是用于管理相关内存的实际指针。该指针变量在多个SharedPtr 对象之间共享,这就是为什么我们需要一个引用计数系统来跟踪在程序生命周期的任何时间点有多少SharedPtr 对象正在管理该指针指向的内存。

          引用计数器 r_count

          这是一个指向整数类型变量的指针,它也被多个管理同一内存的SharedPtr 对象共享。这是共享的,因为每个管理内存的SharedPtr 对象都应该知道管理同一内存的每个其他SharedPtr 对象的计数。实现这一点的方法是拥有一个由同一家族的SharedPtr 对象引用的公共引用计数器。

          每次物化一个新的SharedPtr 对象以管理已由其他SharedPtr 对象/s 管理的内存时,r_count 就会增加1。当SharedPtr 对象死亡时,它也会减少1,以便其他SharedPtr 对象“知道”他们的一位负责管理该家庭所维护的内存的家庭成员已经去世并且不再管理该内存。

          默认构造函数

          当一个新的SharedPtr对象被堆分配的内存创建和初始化时,这个构造函数被调用,内部指针m_ptr被初始化为需要管理的堆分配的内存地址。因为这是对该指针的第一个也是唯一的引用,所以引用计数器 r_count 设置为 1。这里没有发生任何有趣的事情。

          复制构造函数和复制赋值

          这是“真正的”引用计数发生的地方。

          每当使用另一个SharedPtr 对象创建新的SharedPtr 对象或使现有的SharedPtr 引用另一个SharedPtr 时,即基本上是在创建新的SharedPtr 对象(现有的或新创建的)时为了管理一个已经被其他SharedPtr对象/s管理的内存,这个新管理器的内部指针变量m_ptr指向要管理的内存地址,家族的引用计数上升1.

          析构函数

          智能指针旨在释放它们在死亡时管理的内存。对于SharedPtr,它确保在释放内存之前没有其他对正在管理的内存的引用。所有这些都发生在对象的析构函数中。

          正如您在代码中看到的,对象只有在对内存的引用计数为 0 时才会释放内存,然后才会死掉。

          这很重要,因为如果SharedPtr 对象在 r_count 不为 0 时释放内存,则管理相同内存的其他 SharedPtr 对象会在之后的某个时间尝试访问它,结果将是一个程序崩溃。

          SharedPtr 通过将释放内存的责任交给管理内存的最后一个幸存对象来确保不会发生这种情况。由于SharedPtr 的设计,所有这些都自动发生,无需程序员干预。

          这就是引用计数的工作原理。

          引用计数就像几个室友的例行公事:最后离开房间的人有责任锁上大门。为了让这一点无缝地发生,每个室友都应该知道他是否是最后一个离开房间的人。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-10-30
            • 2017-03-20
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-09-14
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多