1.引言 上一篇博文關於淺拷貝和深拷貝 https://www.cnblogs.com/zhaoyixiang/p/12116203.html 我們瞭解到我們在淺拷貝時對帶指針的對象進行拷貝會出現記憶體泄漏,那C++是否可以實現像python,JAVA一樣引入垃圾回收機制,來靈活的來管理記憶體。 遺憾的 ...
1.引言
上一篇博文關於淺拷貝和深拷貝 https://www.cnblogs.com/zhaoyixiang/p/12116203.html
我們瞭解到我們在淺拷貝時對帶指針的對象進行拷貝會出現記憶體泄漏,那C++是否可以實現像python,JAVA一樣引入
垃圾回收機制,來靈活的來管理記憶體。
遺憾的是C++並不像python、java等編程語言一樣有著垃圾回收機制(Gabage Collector),因此導致了C++中對動態
存儲的管理稱為程式員的噩夢,出現了記憶體遺失(memory leak)、懸空指針、非法指針存取等問題。
Bjarne本人認為:
“我有意這樣設計C++,使它不依賴於自動垃圾回收(通常就直接說垃圾回收)。這是基於自己對垃圾回收系統的經驗,
我很害怕那種嚴重的空間和時間開銷,也害怕由於實現和移植垃圾回收系統而帶來的複雜性。還有,垃圾回收將使C+
+不適合做許多底層的工作,而這卻正是它的一個設計目標。但我喜歡垃圾回收的思想,它是一種機制,能夠簡化設計、
排除掉許多產生錯誤的根源。
C++中提供的構造函數和析構函數就是為瞭解決了自動釋放資源的需求。Bjarne有一句名言,“資源需求就是初始化(Resource Inquirment Is Initialization)”。
因此,我們可以將需要分配的資源在構造函數中申請完成,而在析構函數中釋放已經分配的資源。
在C++中,允許你控制對象的創建,清楚和複製,我們就可以通過開發一種稱為引用計數的垃圾回收機制實現這種控制
2.設計思想
首先我們明確在對存在指針的對象構造時,析構對象時需要把指針delete(釋放掉),但是此時如果我們對對象進行淺拷貝,沒有新的指針new。
析構對象時候會出現記憶體泄漏(一個指針所指的記憶體被兩次釋放的清況),我們用通過引用計數來解決這個問題:
每構造一個對象,就創建一個新的計數器並+1.每拷貝構造一次就在被拷貝的那個對象所在的計數器上+1;
析構時候 按照構造函數析構的順序(後造先放,類似棧),最後構造或拷貝的先釋放;
每次釋放先對計數器-1並判斷計數器是否為0(是否存在淺拷貝的對象),大於0時,繼續按照析構順序析構下一個對象;
當計數器為0時,釋放指針。
3.舉例
我們按順序構造3個對象,計數器標號記為 1,2,3,對第一個和第三個對象淺拷貝兩次,
對對象拷貝完成後計數器1,2,3的值分別為 2 1 2.
先釋放計數器3 計數器-1後等於1,析構掉一個對象。計數器為 2 1 1
再釋放計數器1 計數器-1後等於1,析構掉一個對象。計數器為 1 1 1
再釋放計數器3 計數器-1後等於0,析構掉一個對象,並釋放掉指針。計數器為 1 1 空
再釋放計數器2 計數器-1後等於0,析構掉一個對象,並釋放掉指針。計數器為 1 空 空
再釋放計數器1 計數器-1後等於0,析構掉一個對象,並釋放掉指針。計數器為 空 空 空
最終所有對象析構完畢,指針也全部釋放完
4.代碼
//引用計數類 class CRefCount { public: CRefCount(); //構造計數器對象 CRefCount(const CRefCount& obj); //拷貝構造計數器 void* Alloc(int size); //構造對象時申請空間 int AddRef(); //計數增加 int ReleaseRef(); //計數減少 ~CRefCount(); private: void* m_pBuf; //指針緩衝區 int* m_pRefCount; //計數 }; CRefCount::CRefCount() { m_pBuf = nullptr; m_pRefCount = nullptr; } CRefCount::CRefCount(const CRefCount& obj) { m_pBuf = obj.m_pBuf; m_pRefCount = obj.m_pRefCount; AddRef(); } void* CRefCount::Alloc(int size) { m_pBuf = new char[size + 1]; //申請緩衝區 m_pRefCount = new int(0); AddRef(); //每次構造對象計數+1 return m_pBuf; } int CRefCount::AddRef() { if (m_pRefCount == nullptr) return 0; return ++(*m_pRefCount); } int CRefCount::ReleaseRef() { if (m_pRefCount == nullptr) return 0; return --(*m_pRefCount); } CRefCount::~CRefCount() { if (ReleaseRef() == 0) { if (m_pBuf != nullptr) { delete[] m_pBuf; m_pBuf = nullptr; delete m_pRefCount; m_pRefCount = nullptr; } } }
5.測試
//student測試用例 #include"CRefCount.h" #include<iostream> #pragma warning(disable:4996) using namespace std; class CStudent { private: char* m_pName; CRefCount m_RefCount; const char* GetName() const; public: CStudent(const char* pName); }; const char* CStudent::GetName() const { return m_pName; } CStudent::CStudent(const char* pName) { m_pName = (char*)m_RefCount.Alloc(strlen(pName) + 1); //申請一個用來存放名字的空間 strcpy(m_pName, pName); } int main() { CStudent s1("shadow"); CStudent s2("iceice"); CStudent s3("maybe"); CStudent s4 = s1; CStudent s5 = s3; return 0; }
調試這個程式,我們在完成構造和拷貝後,查看記憶體,可以看到此時計數器1,2,3分別對應的值為2,1,2
單步跟入,看到第一個拷貝構造的對象被析構掉,計數器值-1 ,此時3個計數器值分別為為2,1,1
再繼續往後走,發現第二個拷貝對象析構掉切指針所指的記憶體還未被釋放掉,計數器1 -1,此時計數器值為 1,1,1
再向後執行,此時第三個構造的對象開始被析構掉同時計數器減到0,此時對象3的指針被釋放掉。
加上輔助調試代碼,最終可以看到執行結果,構造3次,拷貝2次,釋放3次,完成了引用計數功能