构建一个快速便捷的引用计数类






4.10/5 (13投票s)
文章描述了如何在 C++ 类中实现引用计数,
引言
尽管 C++ 是将模块化面向对象设计与高速控制相结合的绝佳选择,但在内存管理方面通常会变得很繁琐。在复杂的系统中,必须格外小心,以避免内存泄漏和访问未分配内存的尝试。即使是简单的内存管理技术在这种情况下也能提供很大的帮助。在本文中,我将描述引用计数,这是一种最简单的内存管理技术,足以满足许多情况。我将解释如何在 C++ 中实现它,以及如何将其包装在一个简单的指针类中,该类可以“完成”启用类似托管的编程风格,而无需太多的编码麻烦或需要第三方库。
背景
引用计数的概念很简单;每个对象维护一个引用计数器 (RC),该计数器保存指向该对象的指针的数量。每当新指针指向该对象或指向该对象的指针发生更改时,此计数器都会更新。当此计数器达到 0 时,表示该对象不再可被程序访问,因此应被释放。

只要您避免引用循环,引用计数就能很好地工作。如果引用计数管理的对象以循环方式相互指向,最终会得到一组无法被程序访问但其引用计数高于 0 的对象,从而导致内存泄漏。

解决引用循环的直接方法是将指针分类为强指针和弱指针。弱指针不会影响它们指向的对象的引用计数器。您应该在设计中小心,以确保不会形成强指针循环。

实现引用计数
为了在 C++ 中实现引用计数,我们需要定义一个类,该类维护一个引用计数器,支持递增和递减该计数器,并在其计数器达到 0 时销毁和释放自身。
/**
* Base class for all classes that support reference counting
*/
class RCBase
{
public:
RCBase() : mRefCount(0) {}
virtual ~RCBase() {}
void grab() const {++mRefCount;}
void release() const
{
assert(mRefCount > 0);
--mRefCount;
if(mRefCount == 0) {delete (RCBase *)this;}
}
private:
mutable int mRefCount;
};
请注意,我将 mRefCount
声明为 mutable,以便可以通过 const
方法 grab
和 release
来更改它,从而允许引用计数即使在指向常量对象的指针上也能运行。所有我们希望使用引用计数的类都必须直接或间接派生自 RCObject
。该类的用法很简单;要在 MyClass
的对象上维护一个强指针,只需通过 MyClass*
指向它并调用 grab
。一旦您完成了该对象,请调用 release。如果不再有其他地方指向该对象,它将自行删除。请注意,引用计数器初始化为 0,这意味着当您创建一个新对象时,您也必须 grab 它。这将在我们开发指针时变得方便。以下代码示例演示了 RCBase
class MyClass : public RCBase
{
public:
~MyClass() {cout << this << " is no longer needed" << endl;}
void print() {cout << "Hello" << endl;}
};
int main(void)
{
//1: Demonstrate RCBase class
//Module 1 creates an object
MyClass *a = new MyClass();
a->grab(); //RC=1
//Module 2 grabs object
MyClass* ptr = a;
ptr->grab(); //RC=2
//Module 1 no longer needs object
a->release(); //RC=1
a = NULL;
//Module 2 no longer needs object
ptr->release(); //object will be destroyed here
ptr = NULL;
}
通常,一个对象会在函数开始时被 grab,并在函数结束时被 release,或者它会在指向对象的构造函数中被 grab,并在其析构函数中被 release。您会在使用引用计数的框架中找到类似于 RCbase
的基类,例如 Microsoft COM 和 Irrlicht 引擎。虽然到目前为止建立的框架简化了内存管理,但仍然存在两个问题
- 您可能只是忘记 grab 或 release 对象。
- 如果一个对象在一个函数中被 grab,如果该函数由于异常而提前终止,它可能不会被 release。因此,我们需要一个框架,该框架可以根据需要“自动” grab 和 release 对象,并在函数终止时 release 所有在该函数中被 grab 的对象。
我们可以调整标准 C++ 库提供的 auto_ptr
类。 auto_ptr
是一个强指针,每当更改或销毁时,它都会销毁它指向的对象。因此,当保证没有对象会同时拥有多个指向它的强指针时,它很有用。我们所需要做的就是放宽 auto_ptr
类,以便在更改它时,它会 release 它指向的对象并 grab 它将要指向的对象。这会产生一个简单的模板类
/**
* A reference counting-managed pointer for classes derived from RCBase which can
* be used as C pointer
*/
template < class T >
class RCPtr
{
public:
//Construct using a C pointer
//e.g. RCPtr< T > x = new T();
RCPtr(T* ptr = NULL)
: mPtr(ptr)
{
if(ptr != NULL) {ptr->grab();}
}
//Copy constructor
RCPtr(const RCPtr &ptr)
: mPtr(ptr.mPtr)
{
if(mPtr != NULL) {mPtr->grab();}
}
~RCPtr()
{
if(mPtr != NULL) {mPtr->release();}
}
//Assign a pointer
//e.g. x = new T();
RCPtr &operator=(T* ptr)
{
//The following grab and release operations have to be performed
//in that order to handle the case where ptr == mPtr
//(See comment below by David Garlisch)
if(ptr != NULL) {ptr->grab();}
if(mPtr != NULL) {mPtr->release();}
mPtr = ptr;
return (*this);
}
//Assign another RCPtr
RCPtr &operator=(const RCPtr &ptr)
{
return (*this) = ptr.mPtr;
}
//Retrieve actual pointer
T* get() const
{
return mPtr;
}
//Some overloaded operators to facilitate dealing with an RCPtr
//as a conventional C pointer.
//Without these operators, one can still use the less transparent
//get() method to access the pointer.
T* operator->() const {return mPtr;} //x->member
T &operator*() const {return *mPtr;} //*x, (*x).member
operator T*() const {return mPtr;} //T* y = x;
operator bool() const {return mPtr != NULL;} //if(x) {/*x is not NULL*/}
bool operator==(const RCPtr &ptr) {return mPtr == ptr.mPtr;}
bool operator==(const T *ptr) {return mPtr == ptr;}
private:
T *mPtr; //Actual pointer
};
我们的新类现在可以如下使用
//Module 1 creates an object
RCPtr< MyClass > a2 = new MyClass(); //RC=1
//Module 2 grabs object
RCPtr< MyClass > ptr2 = a2; //RC=2
//Module 2 invokes a method
ptr2->print();
(*ptr2).print();
//Module 1 no longer needs object
a2 = NULL; //RC=1
//Module 2 no longer needs object
ptr2 = NULL; //object will be destroyed here
现在,您不再需要担心 grab 和 release 对象。即使 a2 = NULL
和 ptr2 = NULL
行不存在,该对象也会在 a2
和 ptr2
的作用域结束时被正确销毁。您唯一应该关心的是何时使用强指针 (RCPtr
) 以及何时使用弱指针(传统的 C 指针)。
历史
- 2010/03/09 - 首次提交
- 2010/03/16 - 修复了将指针分配给已经指向同一对象的
RCPtr
时导致访问冲突的错误