1. 程式人生 > >智慧指標介紹

智慧指標介紹

 本文將簡要介紹智慧指標shared_ptr和unique_ptr,並簡單實現基於引用計數的智慧指標。

使用智慧指標的緣由

  1. 考慮下邊的簡單程式碼:

1 int main()
2 {
3     int *ptr = new int(0);
4     return 0;
5 }

   就如上邊程式,我們有可能一不小心就忘了釋放掉已不再使用的記憶體,從而導致資源洩漏(resoure leak,在這裡也就是記憶體洩漏)。

   2. 考慮另一簡單程式碼:

1 int main()
2 {
3     int *ptr = new int(0);
4     delete ptr;
5     return 0;
6 }

  我們可能會心想,這下程式應該沒問題了?可實際上程式還是有問題。上邊程式雖然最後釋放了申請的記憶體,但ptr會變成空懸指標(dangling pointer,也就是野指標)。空懸指標不同於空指標(nullptr),它會指向“垃圾”記憶體,給程式帶去諸多隱患(如我們無法用if語句來判斷野指標)。

  上述程式在我們釋放完記憶體後要將ptr置為空,即:

1 ptr = nullptr;

  除了上邊考慮到的兩個問題,上邊程式還存在另一問題:如果記憶體申請不成功,new會丟擲異常,而我們卻什麼都沒有做!所以對這程式我們還得繼續改進(也可用try...catch...):

複製程式碼

 1 #include <iostream>
 2 using namespace std;
 3 
 4 int main()
 5 {
 6     int *ptr = new(nothrow) int(0);
 7     if(!ptr)
 8     {
 9         cout << "new fails."
10         return 0;
11     }
12     delete ptr;
13     ptr = nullptr;
14     return 0;
15 }

複製程式碼

  3. 考慮最後一簡單程式碼:

複製程式碼

 1 #include <iostream>
 2 using namespace std;
 3 
 4 int main()
 5 {
 6     int *ptr = new(nothrow) int(0);
 7     if(!ptr)
 8     {
 9         cout << "new fails."
10         return 0;
11     }
12     // 假定hasException函式原型是 bool hasException()
13     if (hasException())
14         throw exception();
15     
16     delete ptr;
17     ptr = nullptr;
18     return 0;
19 }

複製程式碼

  當我們的程式執行到“if(hasException())”處且“hasException()”為真,那程式將會丟擲一個異常,最終導致程式終止,而已申請的記憶體並沒有釋放掉。

  當然,我們可以在“hasException()”為真時釋放記憶體:

複製程式碼

1 // 假定hasException函式原型是 bool hasException()
2 if (hasException())
3 {
4         delete ptr;
5         ptr = nullptr;
6         throw exception();
7 }

複製程式碼

  但,我們並不總會想到這麼做。而且,這樣子做也顯得麻煩,不夠人性化。

  如果,我們使用智慧指標,上邊的問題我們都不用再考慮,因為它都已經幫我們考慮到了。

  因此,我們使用智慧指標的原因至少有以下三點:

  1)智慧指標能夠幫助我們處理資源洩露問題;

  2)它也能夠幫我們處理空懸指標的問題;

  3)它還能夠幫我們處理比較隱晦的由異常造成的資源洩露。

智慧指標

  自C++11起,C++標準提供兩大型別的智慧指標:

  1. Class shared_ptr實現共享式擁有(shared ownership)概念。多個智慧指標可以指向相同物件,該物件和其相關資源會在“最後一個引用(reference)被銷燬”時候釋放。為了在結構複雜的情境中執行上述工作,標準庫提供了weak_ptr、bad_weak_ptr和enable_shared_from_this等輔助類。

  2. Class unique_ptr實現獨佔式擁有(exclusive ownership)或嚴格擁有(strict ownership)概念,保證同一時間內只有一個智慧指標可以指向該物件。它對於避免資源洩露(resourece leak)——例如“以new建立物件後因為發生異常而忘記呼叫delete”——特別有用。

  注:C++98中的Class auto_ptr在C++11中已不再建議使用。

shared_ptr

  幾乎每一個有分量的程式都需要“在相同時間的多處地點處理或使用物件”的能力。為此,我們必須在程式的多個地點指向(refer to)同一物件。雖然C++語言提供引用(reference)和指標(pointer),還是不夠,因為我們往往必須確保當“指向物件”的最末一個引用被刪除時該物件本身也被刪除,畢竟物件被刪除時解構函式可以要求某些操作,例如釋放記憶體或歸還資源等等。

  所以我們需要“當物件再也不被使用時就被清理”的語義。Class shared_ptr提供了這樣的共享式擁有語義。也就是說,多個shared_ptr可以共享(或說擁有)同一物件。物件的最末一個擁有者有責任銷燬物件,並清理與該物件相關的所有資源。

  shared_ptr的目標就是,在其所指向的物件不再被使用之後(而非之前),自動釋放與物件相關的資源。

  下邊程式摘自《C++標準庫(第二版)》5.2.1節:

複製程式碼

 1 #include <iostream>
 2 #include <string>
 3 #include <vector>
 4 #include <memory>
 5 using namespace std;
 6 
 7 int main(void)
 8 {
 9     // two shared pointers representing two persons by their name
10     shared_ptr<string> pNico(new string("nico"));
11     shared_ptr<string> pJutta(new string("jutta"),
12             // deleter (a lambda function) 
13             [](string *p)
14             { 
15                 cout << "delete " << *p << endl;
16                 delete p;
17             }
18         );
19 
20     // capitalize person names
21     (*pNico)[0] = 'N';
22     pJutta->replace(0, 1, "J");
23 
24     // put them multiple times in a container
25     vector<shared_ptr<string>> whoMadeCoffee;
26     whoMadeCoffee.push_back(pJutta);
27     whoMadeCoffee.push_back(pJutta);
28     whoMadeCoffee.push_back(pNico);
29     whoMadeCoffee.push_back(pJutta);
30     whoMadeCoffee.push_back(pNico);
31 
32     // print all elements
33     for (auto ptr : whoMadeCoffee)
34         cout << *ptr << " ";
35     cout << endl;
36 
37     // overwrite a name again
38     *pNico = "Nicolai";
39 
40     // print all elements
41     for (auto ptr : whoMadeCoffee)
42         cout << *ptr << " ";
43     cout << endl;
44 
45     // print some internal data
46     cout << "use_count: " << whoMadeCoffee[0].use_count() << endl;
47 
48     return 0;
49 }

複製程式碼

  程式執行結果如下:

  

  關於程式邏輯可見下圖:

  

  關於程式的幾點說明:

  1)對智慧指標pNico的拷貝是淺拷貝,所以當我們改變物件“Nico”的值為“Nicolai”時,指向它的指標都會指向新值。

  2)指向物件“Jutta”的有四個指標:pJutta和pJutta的三份被安插到容器內的拷貝,所以上述程式輸出的use_count為4。

  4)shared_ptr本身提供預設記憶體釋放器(default deleter),呼叫的是delete,不過只對“由new建立起來的單一物件”起作用。當然我們也可以自己定義記憶體釋放器,就如上述程式。不過值得注意的是,預設記憶體釋放器並不能釋放陣列記憶體空間,而是要我們自己提供記憶體釋放器,如:

複製程式碼

1 shared_ptr<int> pJutta2(new int[10],
2         // deleter (a lambda function) 
3         [](int *p)
4         { 
5             delete[] p;
6         }
7     );

複製程式碼

   或者使用為unique_ptr而提供的輔助函式作為記憶體釋放器,其內呼叫delete[]:

1 shared_ptr<int> p(new int[10], default_delete<int[]>());

unique_ptr

  unique_ptr是C++標準庫自C++11起開始提供的型別。它是一種在異常發生時可幫助避免資源洩露的智慧指標。一般而言,這個智慧指標實現了獨佔式擁有概念,意味著它可確保一個物件和其相應資源同一時間只被一個指標擁有。一旦擁有者被銷燬或變成空,或開始擁有另一個物件,先前擁有的那個物件就會被銷燬,其任何相應資源也會被釋放。

  現在,本文最開頭的程式就可以寫成這樣啦:

複製程式碼

1 #include <memory>
2 using namespace std;
3 
4 int main()
5 {
6     unique_ptr<int> ptr(new int(0));
7     return 0;
8 }

複製程式碼

智慧指標簡單實現

  基於引用計數的智慧指標可以簡單實現如下(詳細解釋見程式中註釋):

複製程式碼

 1 #include <iostream>
 2 using namespace std;
 3 
 4 template<class T>
 5 class SmartPtr
 6 {
 7 public:
 8     SmartPtr(T *p);
 9     ~SmartPtr();
10     SmartPtr(const SmartPtr<T> &orig);                // 淺拷貝
11     SmartPtr<T>& operator=(const SmartPtr<T> &rhs);    // 淺拷貝
12 private:
13     T *ptr;
14     // 將use_count宣告成指標是為了方便對其的遞增或遞減操作
15     int *use_count;
16 };
17 
18 template<class T>
19 SmartPtr<T>::SmartPtr(T *p) : ptr(p)
20 {
21     try
22     {
23         use_count = new int(1);
24     }
25     catch (...)
26     {
27         delete ptr;
28         ptr = nullptr;
29         use_count = nullptr;
30         cout << "Allocate memory for use_count fails." << endl;
31         exit(1);
32     }
33 
34     cout << "Constructor is called!" << endl;
35 }
36 
37 template<class T>
38 SmartPtr<T>::~SmartPtr()
39 {
40     // 只在最後一個物件引用ptr時才釋放記憶體
41     if (--(*use_count) == 0)
42     {
43         delete ptr;
44         delete use_count;
45         ptr = nullptr;
46         use_count = nullptr;
47         cout << "Destructor is called!" << endl;
48     }
49 }
50 
51 template<class T>
52 SmartPtr<T>::SmartPtr(const SmartPtr<T> &orig)
53 {
54     ptr = orig.ptr;
55     use_count = orig.use_count;
56     ++(*use_count);
57     cout << "Copy constructor is called!" << endl;
58 }
59 
60 // 過載等號函式不同於複製建構函式,即等號左邊的物件可能已經指向某塊記憶體。
61 // 這樣,我們就得先判斷左邊物件指向的記憶體已經被引用的次數。如果次數為1,
62 // 表明我們可以釋放這塊記憶體;反之則不釋放,由其他物件來釋放。
63 template<class T>
64 SmartPtr<T>& SmartPtr<T>::operator=(const SmartPtr<T> &rhs)
65 {
66     // 《C++ primer》:“這個賦值操作符在減少左運算元的使用計數之前使rhs的使用計數加1,
67     // 從而防止自身賦值”而導致的提早釋放記憶體
68     ++(*rhs.use_count);
69 
70     // 將左運算元物件的使用計數減1,若該物件的使用計數減至0,則刪除該物件
71     if (--(*use_count) == 0)
72     {
73         delete ptr;
74         delete use_count;
75         cout << "Left side object is deleted!" << endl;
76     }
77 
78     ptr = rhs.ptr;
79     use_count = rhs.use_count;
80     
81     cout << "Assignment operator overloaded is called!" << endl;
82     return *this;
83 }

複製程式碼

  測試程式如下:

複製程式碼

 1 #include <iostream>
 2 #include "smartptr.h"
 3 using namespace std;
 4 
 5 int main()
 6 {
 7     // Test Constructor and Assignment Operator Overloaded
 8     SmartPtr<int> p1(new int(0));
 9     p1 = p1;
10     // Test Copy Constructor
11     SmartPtr<int> p2(p1);
12     // Test Assignment Operator Overloaded
13     SmartPtr<int> p3(new int(1));
14     p3 = p1;
15     
16     return 0;
17 }

複製程式碼

  測試結果如下:

  

參考資料