1. 程式人生 > >C++智能指針剖析(下)boost::shared_ptr&其他

C++智能指針剖析(下)boost::shared_ptr&其他

剖析 smart_ptr mage open log gin 內部使用 聲明 虛基類

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>
 2
class 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 定制刪除器(仿函數)

經上面分析,我們可以看到,上面的指針不能用於文件的關閉,也不能用於管理mallocnew[]開辟的動態內存的釋放,所以我們可以運用仿函數來定制刪除器。如下:

 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 034 }

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_arrshared_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&其他