30

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

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

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

4

7 回答 7

68

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

  1. 智能指针分配一小块内存来包含引用计数器。然后,智能指针的每个副本都会收到一个指向实际对象的指针和一个指向引用计数的指针。
  2. 除了对象指针之外,每个智能指针还包含前一个和下一个指针,从而形成指向特定对象的智能指针的双向链表。引用计数隐含在列表中。复制智能指针时,它会将自身添加到列表中。销毁后,每个智能指针都会将自己从列表中删除。如果它是列表中的最后一个,那么它也会释放引用的对象。

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

于 2009-04-07T11:26:46.210 回答
3

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

于 2009-04-07T11:19:19.597 回答
3

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

你可以看看这篇文章。此实现将这些存储在一个单独的对象中,该对象被复制。您还可以查看boost 的文档或查看有关智能指针的Wikipedia 文章。

于 2009-04-07T11:19:22.077 回答
2

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

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

于 2009-04-07T11:19:18.740 回答
2

据我记得,在Effective C++的一章中提到了引用计数指针的问题。

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

于 2009-04-07T11:22:49.777 回答
2

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

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

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

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

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

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

于 2009-04-07T13:43:51.263 回答
0

实现 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对象管理的内存时,r_count 就会增加 1。当对象死亡时,它也会减少 1 SharedPtr,以便其他SharedPtr对象“知道”它们的家庭中的一个管理家庭记忆的成员已经去世,不再管理记忆。

默认构造函数

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

复制构造函数和复制赋值

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

每当SharedPtr使用另一个SharedPtr对象创建新对象或使现有对象SharedPtr引用另一个对象SharedPtr时,即基本上当新SharedPtr对象(现有或新创建的)用于管理已由其他SharedPtr对象管理的内存时,内部使这个新管理器的指针变量m_ptr指向要管理的内存地址,并且该族的引用计数增加 1。

析构函数

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

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

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

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

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

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

于 2020-05-14T09:02:44.400 回答