实现 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 的设计,所有这些都自动发生,无需程序员干预。
这就是引用计数的工作原理。
引用计数就像几个室友的例行公事:最后离开房间的人有责任锁上大门。为了让这一点无缝地发生,每个室友都应该知道他是否是最后一个离开房间的人。