智慧指標介紹
本文將簡要介紹智慧指標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 }
測試結果如下: