引用計數在軟件開發中是一項非常重用的技術,它可以說是無處不,我們在不知不覺中都在和它打交道,比如 Windows上的COM和Handle, Mac上的ref句柄,腳本語言中的垃圾回收技術。
但是在C++標准庫中,卻沒有內置支持引用計數的技術的支持,下面我們就嘗試封裝自己的基於引用計數的智能指針。
一般來說,計數方法的實現有2種,內置和外置: 內置指的是對象本身就有計數功能,也就是計數的值變量是對象的成員;外置則是指對象本身不需要支持計數功能,我們是在外部給它加上這個計數能力的。
首先我們來看內置的方法:
封裝一個計數功能的對象CRefObject:
class CRefObject
{
public:
CRefObject()
{
m_nRefCount = 0;
}
int GetRefCount() const
{
return m_nRefCount;
}
int AddRefCount()
{
return ++m_nRefCount;
}
int SubRefCount()
{
return --m_nRefCount;
}
void ResetRefCount()
{
m_nRefCount = 0;
}
private:
int m_nRefCount;
};
然後封裝我們的智能智能CRefPtr<T>:
//T should inherit from CRefObject
template<typename T>
class CRefPtr
{
public:
T* operator->() const
{
return m_pRawObj;
}
T& operator()() const
{
assert(m_pRawObj != NULL);
return *m_pRawObj;
}
T& operator*() const
{
assert(m_pRawObj != NULL);
return *m_pRawObj;
}
T* GetPtr() const
{
return m_pRawObj;
}
bool IsNull() const
{
return m_pRawObj == NULL;
}
explicit CRefPtr(T* p = NULL)
{
m_pRawObj = p;
if(p != NULL)
{
p->AddRefCount();
}
}
CRefPtr(const CRefPtr& ref)
{
m_pRawObj = ref.m_pRawObj;
if(m_pRawObj != NULL)
{
m_pRawObj->AddRefCount();
}
}
~CRefPtr()
{
if(m_pRawObj != NULL && m_pRawObj->SubRefCount() == 0)
{
delete m_pRawObj;
}
}
CRefPtr& operator = (const CRefPtr& ref)
{
if(this != &ref)
{
if(m_pRawObj != NULL
&& m_pRawObj->SubRefCount() == 0)
{
delete m_pRawObj;
}
m_pRawObj = ref.m_pRawObj;
if(m_pRawObj != NULL)
{
m_pRawObj->AddRefCount();
}
}
return *this;
}
bool operator == (const CRefPtr& ref) const
{
return m_pRawObj == ref.m_pRawObj;
}
private:
T* m_pRawObj;
};
通過上面的代碼可以看到,我們要求要支持引用計數的對象都要從CRefObject繼承,也就是給這個對象內置計數功能。
然後我們就可以這樣使用了:
#include <iostream>
using namespace std;
#include "RefPtr.h"
class CTest: public CRefObject
{
public:
CTest(int n)
:m_n(n)
{
cout << "CTest(" << m_n << ") \n";
}
~CTest()
{
cout << "~CTest(" << m_n << ") \n";
}
void Print()
{
cout << m_n << "\n";
}
int m_n;
};
int main(int argc, char* argv[])
{
{
CRefPtr<CTest> p1(new CTest(1));
CRefPtr<CTest> p2(new CTest(2));
p1->Print();
p1 = p2;
}
system("pause");
return 0;
}
接下來我們嘗試實現據通過外置方法實現引用計數的智能指針CRefIPtr<T>, 代碼如下:
template<typename T>
class CRefIPtr
{
public:
T* operator->() const
{
return GetObjectPtr();
}
T& operator()() const
{
return GetObject();
}
T& operator*() const
{
return GetObject();
}
T* GetPtr() const
{
return GetObjectPtr();
}
bool IsNull() const
{
return (m_pHolder != NULL
&& m_pHolder->m_pRawObj != NULL);
}
explicit CRefIPtr(T* p = NULL)
{
m_pHolder = new CRefHolder;
if(m_pHolder != NULL)
{
m_pHolder->m_pRawObj = p;
m_pHolder->AddRefCount();
}
}
CRefIPtr(const CRefIPtr& ref)
{
m_pHolder = ref.m_pHolder;
if(m_pHolder != NULL)
{
m_pHolder->AddRefCount();
}
}
~CRefIPtr()
{
if(m_pHolder != NULL
&& m_pHolder->SubRefCount() == 0)
{
delete m_pHolder;
}
}
CRefIPtr& operator = (const CRefIPtr& ref)
{
if(this != &ref
&& m_pHolder != ref.m_pHolder)
{
if(m_pHolder != NULL
&& m_pHolder->SubRefCount() == 0)
{
delete m_pHolder;
}
m_pHolder = ref.m_pHolder;
if(m_pHolder != NULL)
{
m_pHolder->AddRefCount();
}
}
return *this;
}
bool operator == (const CRefIPtr& ref) const
{
return m_pHolder == ref.m_pHolder;
}
protected:
T& GetObject() const
{
assert(m_pHolder != NULL
&& m_pHolder->m_pRawObj != NULL);
return *(m_pHolder->m_pRawObj);
}
T* GetObjectPtr() const
{
if(m_pHolder != NULL)
{
return m_pHolder->m_pRawObj;
}
else
{
return NULL;
}
}
class CRefHolder: public CRefObject
{
public:
CRefHolder()
{
m_pRawObj = NULL;
}
~CRefHolder()
{
delete m_pRawObj;
}
T* m_pRawObj;
};
private:
CRefHolder* m_pHolder;
};
可以看到在外置的方法中我們內部封裝了一個具有計數功能的CRefHolder, 通過它實現我們的計數功能, 具體用法和上面CRefPtr類似,只不過CRefIPtr不再強制要求對象從CRefObject繼承。
下面我們來討論這2種方法的優缺點:
(1)從性能上來說,肯定內置的高,因為它不用通過新建內部Holder對象。
(2) 從易用性上來說, 外置的更方便,因為它不強制要求對象從CRefObject繼承。
(3) 從使用范圍上說, 外置的更廣闊, 因為外置的方法支持C++ 內置類型也很方便, 比如CRefIPtr<int> p(new int(1)), 內置的卻做不到。
但是外置的比內置在使用不當的情況下,有時更容易出錯,比如下面的代碼:
int main(int argc, char* argv[])
{
{
CRefPtr<CTest> p1(new CTest(1));
CTest* pRaw = p1.GetPtr();
CRefPtr<CTest> p2(pRaw);
}
system("pause");
return 0;
}
用CRefPtr運行正常,但是改成CRefIPtr時,卻會Crash。
究其原因是在內置情況下我們可以知道原始對象內部的計數值,但是外置情況下就無能為力了。
當然上面的用法本身就是不規范的,就像你這樣用:
int main(int argc, char* argv[])
{
{
CTest t(1);
CRefPtr<CTest> p2(&t);
}
system("pause");
return 0;
}
上面代碼,無論用內置還是外置,都會Crash。
當然,我們上面的2種引用計數智能指針在實現上都沒有考慮多線程的情況,多線程情況只要給CRefObject加鎖就可以了。
基於引用計數智能指針還有一個致命的缺點就是循環引用,會造成對象沒法自動釋放,這種情況下需要我們在需要釋放對象時手動將指針值設成NULL。
總之,如果我們要在正式項目中使用這種方式的智能指針,使用者要對它的內部機制有深入的理解,同時建議不要同時混用智能指針和原始指針,另外建議只在模塊內部使用,而不要跨模塊傳遞智能指針。
上面是我對引用計數智能指針的一些理解和看法,如果有不正確的地方,歡迎指正。
摘自 厚積薄發