0、異常安全
C++沒有內存回收機制,每次程序員new出來的對象需要手動delete,流程復雜時可能會漏掉delete,導致內存泄漏。於是C++引入智能指針,可用於動態資源管理,資源即對象的管理策略。
使用 raw pointer 管理動態內存時,經常會遇到這樣的問題:
- 忘記
delete
內存,造成內存泄露。 - 出現異常時,不會執行
delete
,造成內存泄露。
下面的代碼解釋了,當一個操作發生異常時,會導致delete
不會被執行:
1 void func() 2 { 3 auto ptr = new Widget; 4 // 執行一個會拋出異常的操作 5 func_throw_exception(); 6 7 delete ptr; 8 }
在C++98中,為了寫出異常安全的代碼,代碼經常寫的很笨拙,如下:
1 void func() 2 { 3 auto ptr = new Widget; 4 try { 5 func_throw_exception(); 6 } 7 catch(...) { 8 delete ptr; 9 throw; 10 } 11 delete ptr; 12 }
使用智能指針能輕易寫出異常安全的代碼,因為當對象退出作用域時,智能指針將自動調用對象的析構函數,避免內存泄露。
一、智能指針shared_ptr
智能指針主要有三種:shared_ptr,unique_ptr和weak_ptr。
shared_ptr
shared_ptr是最常用的智能指針(項目中我只用過shared_ptr)。shared_ptr采用了引用計數器,多個shared_ptr中的T *ptr指向同一個內存區域(同一個對象),並共同維護同一個引用計數器。shared_ptr定義如下,記錄同一個實例被引用的次數,當引用次數大於0時可用,等於0時釋放內存。
注意避免循環引用,shared_ptr的一個最大的陷阱是循環引用,循環,循環引用會導致堆內存無法正確釋放,導致內存泄漏。循環引用在weak_ptr中介紹。
1 temple<typename T> 2 class SharedPtr { 3 public: 4 ... 5 private: 6 T *_ptr; 7 int *_refCount; //should be int*, rather than int 8 };
shared_ptr對象每次離開作用域時會自動調用析構函數,而析構函數並不像其他類的析構函數一樣,而是在釋放內存是先判斷引用計數器是否為0。等於0才做delete操作,否則只對引用計數器左減一操作。
1 ~SharedPtr() 2 { 3 if (_ptr && --*_refCount == 0) { 4 delete _ptr; 5 delete _refCount; 6 } 7 }
接下來看一下構造函數,默認構造函數的引用計數器為0,ptr指向NULL:
1 SharedPtr() : _ptr((T *)0), _refCount(0) 2 { 3 }
用普通指針初始化智能指針時,引用計數器初始化為1:
1 SharedPtr(T *obj) : _ptr(obj), _refCount(new int(1)) 2 { 3 } //這里無法防止循環引用,若我們用同一個普通指針去初始化兩個shared_ptr,此時兩個ptr均指向同一片內存區域,但是引用計數器均為1,使用時需要注意。
拷貝構造函數需要注意,用一個shared_ptr對象去初始化另一個shared_ptr對象時,引用計數器加一,並指向同一片內存區域:
1 SharedPtr(SharedPtr &other) : _ptr(other._ptr), _refCount(&(++*other._refCount)) 2 { 3 }
賦值運算符的重載
當用一個shared_ptr<T> other去給另一個 shared_ptr<T> sp賦值時,發生了兩件事情:
一、sp指針指向發生變化,不再指向之前的內存區域,所以賦值前原來的_refCount要自減
二、sp指針指向other.ptr,所以other的引用計數器_refCount要做++操作。
1 SharedPtr &operator=(SharedPtr &other) 2 { 3 if(this==&other) 4 return *this; 5 6 ++*other._refCount; 7 if (--*_refCount == 0) { 8 delete _ptr; 9 delete _refCount; 10 } 11 12 _ptr = other._ptr; 13 _refCount = other._refCount; 14 return *this; 15 }
定義解引用運算符,直接返回底層指針的引用:
1 T &operator*() 2 { 3 if (_refCount == 0) 4 return (T*)0; 5 6 return *_ptr; 7 }
定義指針運算符->
1 T *operator->() 2 { 3 if(_refCount == 0) 4 return 0; 5 6 return _ptr; 7 }
二、測試
1 int main(int argc, const char * argv[]) 2 { 3 SharedPtr<string> pstr(new string("abc")); 4 SharedPtr<string> pstr2(pstr); 5 SharedPtr<string> pstr3(new string("hao")); 6 pstr3 = pstr2; 7 8 return 0; 9 }
為了讓測試結果更明顯,我在方法中加入了一些輸出,測試結果如下:
源碼鏈接:https://github.com/guhowo/test/tree/master/cplus/SharedPtr
思考
1、本文這種寫法不是線程安全的,是吧?
2、boost中的shared_ptr線程安全嗎?