问题:
为什么要用引用计数?
例如,有3各类class A class B 和 class C 他们分别都用到了Data* pdata,此时谁负责删除pdata呢?答案是最后一个使用到pdata的类来删除。假设class A创建了pdata,但是它却没有权利删除pdata,只有最后一个类才能删除pdata。因为如果让class A来删除pdata,此时class b 和 class c还在使用pdata,一旦class B 和 class C用到了pData,则程序会出现段错误,因为pdata已经被class A删除。
如何实现引用计数?
分析: 实现一个根据计数器的值为零时,删除自身的类,该类应该有这样的实现,
If( count == 0)
delete obj;
这上面牵涉到两个重要概念,一个是计数器,一个是析构操作。计数器什么时候加1,什么时候减1,还有就是什么时候进行删除操作。这些问题都必须清楚才能够实现一个具有引用计数能力的类。
首先回答,计数器什么时候加1,什么时候减1。继续上面的实例,如果ClassA创建了一个具有引用能力的类Data* pdata,此时计数器应该加1, 表示当前有一个实例正在使用pdata。如果class B也绑定到了 pdata,那么这个时候计数器也要加1,一次类推,pdata每次一另外的类绑定的时候计数器都必须加1;如果这个时候有个类解除了对pdata的绑定,计数器这时候就要减1。下面是使用的例子
ClassA a;//这时,a实例创建了pdata, pdata 的计数器为1。
ClassB b(a.getData()) b实例于pdata进行了绑定,所以pdata计数器变2。
ClassC c(a.getData()); // 实例c绑定pdata,pdata计数器变3。
那什么时候解除绑定,一般情况是类被析构的时候会解除绑定,但这不是一定,还得根据需求具体判断什么时候解除绑定。所以需要在类的析构函数里面解除对pdata的绑定。
其次,什么时候删除?答案显而易见。当计数器为0的时候对类进行删除。因为计数器为0意味着没有类绑定到这个类,所以可以进行安心的删除了。
下面是对这个例子简单实现。
class Data { public: Data():i(0),d(0.0),f(0.0f){ ref(); } void ref(){ count++; printf("increase count:%d\n", count); } void unref() { count--; printf("decrease count:%d\n", count); if( count == 0) { printf("delete data\n"); delete this; } } private: int count; int i; double d; float f; }; class ClassA { public: ClassA(){ pdata = new Data();//创建Data,计数器的值为1 } Data* getData() { return pdata; } ~ClassA() { printf("destructor ClassA\n"); pdata->unref(); } private: Data* pdata; }; class ClassB { public: ClassB(Data* data):pdata(data){ pdata->ref(); //绑定Data,计数器加1 } ~ClassB(){ printf("destructor ClassB\n"); pdata->unref();//解除Data绑定,计数器减1 } private: Data* pdata; }; class ClassC { public: ClassC(Data* data):pdata(data){ pdata->ref(); //绑定Data,计数器加1 } ~ClassC() { printf("destructor ClassC\n"); pdata->unref();//解除Data绑定,计数器减1 } private: Data* pdata; }; int main(int argc, const char *argv[]) { ClassA a; ClassB b(a.getData()); ClassC c(a.getData()); return 0; }
程序输出:
increase count:1
increase count:2
increase count:3
destructor ClassC
decrease count:2
destructor ClassB
decrease count:1
destructor ClassA
decrease count:0
delete data
一个万能引用计数类
上面的实现是有局限的,常常一个工程中可能会用到多个引用计数类,这些类每次实现的时候都要重写一遍ref() unref()。这样很不方便。有没有可能做到复用引用计数类。当然,通过继承可以实现一个万用的引用计数类。当子类继承引用计数类作为父类,那么子类也就拥有了父类的引用计数的能力。
下面是简单的实现
//引用计数基类 class RefCnt { public: RefCnt():cnt(0){ref();} void ref(){ cnt++;} void unref(){ cnt--; if( cnt == 0) delete this; } public: virtual ~RefCnt(){ //析构函数要用"virtual"声明 cnt = 0; } private: int cnt; }; //Test类继承引用计数基类 class Data : public RefCnt { public: Data (){ printf("test construct\n");} Data (const Test& obj){ } ~ Data (){ printf("test destructor\n");} private: int i; double d; float f; };
注意:定义引用计数器基类的时候一定在它析构函数前边加virtual, 否则delete this;的时候只会析构基类而不会析构子类。
如果用户这样使用
Data data;
classA* a = new ClassA(&data);
classB* b = new ClassB(&data);
这个时候会出现问题,a和b都绑定了data,而data因为它不是指针,他随着作用域的结束自己释放内存,此时a和b仍然绑定着data,如果a和b使用data就会出现段错误。
所以具有引用计数类只能用指针来使用才安全。引用计数的提出是为了管理堆内存,使用引用计数也就只限于指针。如果堆栈的对象用到了引用计数,就会出现错误。
另一个万用引用计数——智能指针
用模板也可以实现引用计数,前面的实现还有一个缺点,每次绑定和解除的时候用户都需要显示的调用ref()和unRef()来增加和减少计数器。 能不能不要这么麻烦,让类自己根据时机来增加和减少计数器。智能指针横空出世来满足这样的需求。
先来看看如何使用智能指针
Class A { public: A(){} void fun1(){} void func2(){} void func3(){} }; int main() { shared_ptr<A> pa(new A()); pa->fun1(); pa->fun2(); pa->fun3(); } class B { public: B(): mData(new A()){ } shared_ptr<A> getData(){ return mData;} private: shared_ptr<A> mData } class C { public: C():mData(shared_ptr<A> data):mData(data){} private: shared_ptr<A> mData; } int main() { B b; C c(b.getData());//b 和 c 都绑定了A }
用模板实现的引用计数具有很大的灵活性,上个例子中,A不用特别实现为引用的类,只需要将它的对象传到智能指针里面,它就具备了引用计数的能力,前提是必须通过智能指针来操作A。用户不应该直接操作A的对象。其次,用户不用再关心计数器如何操作,这些操作都被智能指针代劳了。
如何实现一个模板的只能指针?这里用的其实是代理技术,在这里智能指针代理类A的所有操作。所以实现智能指针必须能够让用户想访问被代理类一样访问智能指针。第二个需要关心的问题依然是如何管理计数器。因为管理计数器类的职能不被开放给用户,所以这里必须实现 何时对计数器进行加减操作。
何时对计数器进行加减操作? 这里有个微妙的观察,上面的例子中,绑定发生在这条语句
C c(b.getData()); 这里执行的两条语句 return mData;和 c():mData(data){}都说明了绑定的实际发生在智能指针拷贝的时候。所以实现计数器的操作应该在拷贝构造函数里面实现。
下面是智能指针的实现
template<class T> class shared_ptr { public: shared_ptr(T* pData):mData(pData),mCount(new int(1)){} shared_ptr(const shared_ptr<T>& obj) :mCount(0), mData(obj.mData) { (*obj.mCount)++; printf("+count:%d\n", *obj.mCount); this->mCount = obj.mCount; } shared_ptr<T>& operator = (const shared_ptr<T>& obj) { if( this != &obj) { (*obj.mCount)++; printf("+count:%d\n", *obj.mCount); mCount = obj.mCount; mData = obj.mdata; } } ~shared_ptr(){ (*mCount)--; printf("-count:%d\n", *mCount); if( (*mCount) == 0) { printf("delete mData\n"); delete mData; delete mCount; } } T* operator ->() { return mData; } T& operator *() { return *mData; } bool operator!() { reutnr mData != 0; } T* get() { return mData; } private: T* mData; int* mCount; };
为了像普通指针那样操作智能指针,我们还需要操作符
T* operator->();
T& opeator *();
bool operator !();
智能指针有个缺点,在一智能指针为参数的函数只能指定某个特定的类,不能做到通用。
总结
引用计数的出现是为了更好的管理堆内存上的对象,能够让程序员从释放内存的繁琐工作中释放出来。值得注意的是要管理的对象应该是堆上的而不是堆栈上的对象。