C++智能指針及其簡單實現


  本文將簡要介紹智能指針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 }

  測試結果如下:

  

參考資料

  《C++標准庫(第二版)》  

  C++中智能指針的設計和使用

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM