C++智能指針剖析(下)boost::shared_ptr&其他
1. boost::shared_ptr
前面我已經講解了兩個比較簡單的智能指針,它們都有各自的優缺點。由於 boost::scoped_ptr 獨享所有權,當我們真真需要復制智能指針時,需求便滿足不了了,如此我們再引入一個智能指針,專門用於處理復制,參數傳遞的情況,這便是如下的boost::shared_ptr。
boost::shared_ptr 屬於 boost 庫,定義在 namespace boost 中,包含頭文件#include<boost/smart_ptr.hpp> 便可以使用。在上面我們看到 boost::scoped_ptr 獨享所有權,不允許賦值、拷貝,boost::shared_ptr 是專門用於共享所有權的,由於要共享所有權,其在內部使用了引用計數。boost::shared_ptr 也是用於管理單個堆內存對象的。
這是比較完善的一個智能指針,他是通過指針保持某個對象的共享擁有權的智能指針。若幹個shared_ptr對象可以擁有同一個對象,該對象通過維護一個引用計數,記錄有多少個shared_ptr指針指向該對象,最後一個指向該對象的shared_ptr被銷毀或重置時,即引用計數變為0時,該對象被銷毀。銷毀對象時使用的是delete表達式或是在構造shared_ptr時傳入的自定義刪除器(delete),這後面會有詳細講解,但是shared_ptr指針同樣擁有缺陷,那就是循環引用,和線程安全問題,這也在後面講解。先來模擬實現一下shared_ptr指針。
1 template <class T> 2class SharedPtr 3 { 4 public: 5 SharedPtr(T* ptr = NULL) 6 :_ptr(ptr) 7 ,_count(new int(0)){ 8 if (_ptr != NULL) { 9 ++(*_count); 10 } 11 } 12 SharedPtr(const SharedPtr<T>& sp) 13 :_ptr(sp._ptr) 14 ,_count(sp._count){15 if (_ptr != NULL) { 16 ++(*_count); 17 } 18 } 19 SharedPtr<T>& operator=(const SharedPtr<T>& sp) { 20 if (this != &sp) { //排除對象本身自己給自己賦值 21 if (--(*_count) <= 0) { 22 delete[]_ptr; 23 delete[]_count; 24 } 25 else //指向同一個對象的指針互相賦值 26 { } 27 _ptr = sp, _ptr; 28 _count = sp._count; 29 *(_count)++; 30 } 31 return *this; 32 } 33 ~SharedPtr() { 34 if (--(*count) == 0) { 35 delete[] _ptr; 36 delete[] _count; 37 } 38 } 39 T& operator*(){ 40 return *_ptr; 41 } 42 T* operator->() { 43 return _ptr; 44 } 45 bool operator ==(const SharedPtr<T>& sp) { 46 return (_ptr == sp._ptr); 47 } 48 bool operator !=(const SharedPtr<T>& sp) { 49 return (_ptr != sp._ptr); 50 }
51 int UseCount() {
52 return *_count;
53 }
51 private: 52 T* _ptr; 53 int* _count; 54 };
1.1 問題1:線程安全
因為使用引用計數值位判定指標,所以在多線程的環境下是不安全的。會因線程調用的先後順序不同導致錯誤產生。對於這種問題,解決方法一般是加鎖,對引用計數進行加鎖,保證操作是互斥的。
1.2 問題2:循環引用
針對循環引用,從實際的例子來大分析問題,以便能更好的理解。看下面代碼:
1 struct ListNode 2 { 3 int _data; 4 SharedPtr<ListNode> _next; 5 SharedPtr<ListNode> _prev; 6 7 ListNode(int x) 8 :_data(x), _next(NULL), _prev(NULL) 9 {} 10 ~ListNode() 11 { 12 cout << "~ListNode()" << endl; 13 } 14 }; 15 16 void test() 17 { 18 SharedPtr<ListNode> A(new ListNode(1)); 19 SharedPtr<ListNode> B(new ListNode(2)); 20 21 if (A && B) { 22 A->_next = B; 23 B->_prev = A; 24 } 25 26 cout << "A._count:" << A.UseCount() << endl; 27 cout << "B._count:" << B.UseCount() << endl; 28 } 29 30 int main() 31 { 32 test(); 33 getchar(); 34 return 0; 35 }
輸出結果:從結果可以看出兩個節點的引用計數都是2,且一直沒有調用析構函數,這將造成內存泄漏,下面我將圖解原理:而要解決循環引用的問題,就需要用boost庫的另一個智能指針,即boost::weak_ptr。後面在詳細講解。
1.3 定制刪除器(仿函數)
經上面分析,我們可以看到,上面的指針不能用於文件的關閉,也不能用於管理malloc和new[]開辟的動態內存的釋放,所以我們可以運用仿函數來定制刪除器。如下:
1 template<class T> 2 struct DeleteArray //用於new[]開辟的動態內存釋放 3 { 4 void operator()(T* ptr) 5 { 6 cout << "A" << endl; 7 delete[] ptr; 8 } 9 }; 10 struct Fclose //用於文件關閉 11 { 12 void operator()(FILE* ptr) 13 { 14 cout << "B" << endl; 15 16 fclose(ptr); 17 } 18 }; 19 template<class T> 20 struct Free //用於malloc開辟的動態內存的釋放 21 { 22 void operator()(T* ptr) 23 { 24 cout << "C" << endl; 25 free(ptr); 26 } 27 }; 28 int main() 29 { 30 shared_ptr<string> ap1(new string[10], DeleteArray<string>()); 31 shared_ptr<FILE> ap2(fopen("test.txt", "w"),Fclose()); 32 shared_ptr<int> ap3((int*)malloc(sizeof(int)), Free<int>()); 33 return 0; 34 }
2. boost::weak_ptr
boost::weak_ptr 屬於 boost 庫,定義在 namespace boost 中,包含頭文件#include<boost/smart_ptr.hpp> 便可以使用。
在講 boost::weak_ptr 之前,讓我們先回顧一下前面講解的內容。似乎boost::scoped_ptr、boost::shared_ptr 這兩個智能指針就可以解決所有單個對象內存的管理了,這兒還多出一個 boost::weak_ptr,是否還有某些情況我們沒納入考慮呢?答案是有。首先 boost::weak_ptr 是專門為 boost::shared_ptr 而準備的。有時候,我們只關心能否使用對象,並不關心內部的引用計數。boost::weak_ptr 是 boost::shared_ptr 的觀察者(Observer)對象,觀察者意味著 boost::weak_ptr 只對 boost::shared_ptr 進行引用,而不改變其引用計數,當被觀察的 boost::shared_ptr 失效後,相應的 boost::weak_ptr 也相應失效。weak_ptr其實是一個輔助性的智能指針,結合shared_ptr指針使用,它的本質就是弱引用,並不增加引用計數值。他沒有實現->和*運算符的重載,所以不能直接用它訪問對象。針對循環引用這個問題,就是因為不會引起引用計數值的改變,所以我們可以將_next和_prev定義為weak_ptr指針,這樣就很好地解決了循環引用的問題。
1 struct ListNode 2 { 3 int _data; 4 weak_ptr<ListNode> _next; //定義為weak_ptr指針 5 weak_ptr<ListNode> _prev; 6 7 ListNode(int x) 8 :_data(x), _next(NULL), _prev(NULL) 9 {} 10 ~ListNode() 11 { 12 cout << "~ListNode()" << endl; 13 } 14 };
其實 boost::weak_ptr 主要用在軟件架構設計中,可以在基類(此處的基類並非抽象基類,而是指繼承於抽象基類的虛基類)中定義一個 boost::weak_ptr,用於指向子類的boost::shared_ptr,這樣基類僅僅觀察自己的 boost::weak_ptr 是否為空就知道子類有沒對自己賦值了,而不用影響子類 boost::shared_ptr 的引用計數,用以降低復雜度,更好的管理對象。
boost庫剩余的兩個指針:auto_arr和shared_arr.這兩個都是管理數組的,因為之前幾個指針的析構函數中都是delete,不能對數組進行釋放,所以我們自己定制刪除器,而這兩個指針就是專門管理指針的。下面來模擬實現一下。
3. 模擬實現auto_arr
1 template<class T> 2 class AutoArr 3 { 4 public: 5 AutoArr(T* ptr = NULL) 6 :_ptr(ptr) 7 {} 8 9 ~AutoArr() 10 { 11 delete[] _ptr; 12 } 13 14 AutoArr(const AutoArr<T>& s) 15 { 16 _ptr = s._ptr; 17 s._ptr = NULL; 18 } 19 20 AutoArr<T>& operator=(const AutoArr<T>& s) 21 { 22 if (this != &s) 23 { 24 _ptr = s._ptr; 25 s._ptr = NULL; 26 } 27 return *this; 28 } 29 30 T& operator[](size_t pos) 31 { 32 if (_ptr == NULL) 33 { 34 throw a; 35 } 36 return *(_ptr+pos); 37 } 38 39 void set(T* ptr) 40 { 41 int i = 0; 42 while (*(ptr + i)) 43 { 44 *(_ptr + i) = *(ptr + i); 45 i++; 46 } 47 } 48 49 protected: 50 T* ptr; 51 };
4. 模擬實現shared_arr
1 template<class T> 2 3 class SharedArr 4 { 5 public: 6 SharedArr(T* ptr = NULL) 7 :_ptr(ptr),_count(new int(0)) 8 { 9 (*_count)++; 10 } 11 12 ~SharedArr() 13 { 14 delete[] _ptr; 15 } 16 17 SharedArr(const SharedArr<T>& s) 18 { 19 _ptr = s._ptr; 20 (*_count)++; 21 } 22 23 SharedArr<T>& operator=(const SharedArr<T>& s) 24 { 25 if (this != &s) 26 { 27 if (--(*_count) <= 0) 28 { 29 delete _ptr; 30 delete _count; 31 } 32 else 33 { } 34 _ptr = s._ptr; 35 _count = s._count; 36 (*_count)++; 37 } 38 } 39 40 T& operator[](size_t pos) 41 { 42 if (_ptr == NULL) 43 { 44 throw 1; 45 } 46 return *(_ptr + pos); 47 } 48 49 protected: 50 T* _ptr; 51 int* _count; 52 };
5. 如何選擇智能指針?
在掌握了這幾種智能指針後,大家可能會想另一個問題:在實際應用中,應使用哪種智能指針呢?下面給出幾個使用指南。
(1)如果程序要使用多個指向同一個對象的指針,應選擇shared_ptr。這樣的情況包括:
- 有一個指針數組,並使用一些輔助指針來標示特定的元素,如最大的元素和最小的元素;
- 兩個對象包含都指向第三個對象的指針;
- STL容器包含指針。很多STL算法都支持復制和賦值操作,這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出warning)和auto_ptr(行為不確定)。如果你的編譯器沒有提供shared_ptr,可使用Boost庫提供的shared_ptr。
(2)如果程序不需要多個指向同一個對象的指針,則可使用unique_ptr。如果函數使用new分配內存,並返還指向該內存的指針,將其返回類型聲明為unique_ptr是不錯的選擇。這樣,所有權轉讓給接受返回值的unique_ptr,而該智能指針將負責調用delete。可將unique_ptr存儲到STL容器在那個,只要不調用將一個unique_ptr復制或賦給另一個算法(如sort())。例如,可在程序中使用類似於下面的代碼段。
1 unique_ptr<int> make_int(int n) 2 { 3 return unique_ptr<int>(new int(n)); 4 } 5 void show(unique_ptr<int> &p1) 6 { 7 cout << *a << ‘ ‘; 8 } 9 int main() 10 { 11 ... 12 vector<unique_ptr<int> > vp(size); 13 for(int i = 0; i < vp.size(); i++) 14 vp[i] = make_int(rand() % 1000); // copy temporary unique_ptr 15 vp.push_back(make_int(rand() % 1000)); // ok because arg is temporary 16 for_each(vp.begin(), vp.end(), show); // use for_each() 17 ... 18 }
其中push_back調用沒有問題,因為它返回一個臨時unique_ptr,該unique_ptr被賦給vp中的一個unique_ptr。另外,如果按值而不是按引用給show()傳遞對象,for_each()將非法,因為這將導致使用一個來自vp的非臨時unique_ptr初始化pi,而這是不允許的。前面說過,編譯器將發現錯誤使用unique_ptr的企圖。
在unique_ptr為右值時,可將其賦給shared_ptr,這與將一個unique_ptr賦給一個需要滿足的條件相同。與前面一樣,在下面的代碼中,make_int()的返回類型為unique_ptr<int>:
1 unique_ptr<int> pup(make_int(rand() % 1000)); // ok 2 shared_ptr<int> spp(pup); // not allowed, pup as lvalue 3 shared_ptr<int> spr(make_int(rand() % 1000)); // ok
模板shared_ptr包含一個顯式構造函數,可用於將右值unique_ptr轉換為shared_ptr。shared_ptr將接管原來歸unique_ptr所有的對象。
在滿足unique_ptr要求的條件時,也可使用auto_ptr,但unique_ptr是更好的選擇。如果你的編譯器沒有unique_ptr,可考慮使用Boost庫提供的scoped_ptr,它與unique_ptr類似。
C++智能指針剖析(下)boost::shared_ptr&其他