C++筆記(11) 智慧指針
- 2021 年 8 月 28 日
- 筆記
1. 設計思想
智慧指針是行為類似於指針的類對象,但這種對象還有其他功能。首先,看下面的函數:
void remodel(std::string & str) { std::string * ps = new std::string(str); ... if (weird_thing()) throw exception(); str = *ps; delete ps; return; }
當出現異常時(weird_thing()返回true),delete將不被執行,因此將導致記憶體泄露。可以用上一章介紹的方式修復這種問題:
void remodel(std::string & str) { std::string * ps = new std::string(str); ... try{ if (weird_thing()) throw exception(); } catch(exception &ex){ delete ps; throw; } str = *ps; delete ps; return; }
然而這將增加疏忽和產生其他錯誤的機會。
我們需要的是,當remodel函數中止時(不管是正常中止還是異常中止),本地變數都將從棧記憶體中刪除,即指針ps佔據的記憶體將被釋放,同時ps指向的記憶體也被釋放。
如果ps有一個析構函數,該析構函數在ps過期時釋放它指向的記憶體。但問題在於,ps只是一個常規指針,不是有析構凼數的類對象。如果它是對象,則可以在對象過期時,讓它的析構函數刪除指向的記憶體。這正是智慧指針背後的思想。
(我的理解是,將指針封裝成為類,其析構函數可以釋放指針佔用的記憶體,在析構函數中增加delete釋放它指向的記憶體。)
下面是使用智慧指針auto_ptr修改該函數的結果:
# include <memory> //1.包含頭義件memory(智慧指針所在的頭文件) void remodel (std::string & str) { std::auto_ptr<std::string> ps (new std::string(str));//2.將指向string的指針替換為指向string的智慧指針對象 ...//智慧指針模板在名稱空間std中 if (weird_thing ()) throw exception(); str = *ps; // delete ps; 3. 刪除delete語句 return; }
2. 使用智慧指針
STL一共給我們提供了四種智慧指針:auto_ptr、unique_ptr、shared_ptr和weak_ptr(本文暫不討論)。模板auto_ptr是C++98提供的解決方案,C+11已將將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了好多年;同時,如果您的編譯器不支援其他兩種解決力案,auto_ptr將是唯一的選擇。
要創建智慧指針對象,
- 必須包含頭文件memory,其中,auto_ptr的類模板原型為:
template <class _Ty> class auto_ptr { // wrap an object pointer to ensure destruction public: using element_type = _Ty; explicit auto_ptr(_Ty* _Ptr = nullptr) noexcept : _Myptr(_Ptr) {} ... }
- 然後使用使用通常的模板語法來實例化所需要類型的指針。
auto_ptr<string> pa1(new string("auto"));
注意事項:
1. 所有的智慧指針類都有一個explicit構造函數,以指針作為參數。因此不能自動將指針轉換為智慧指針對象,必須顯式調用:
shared_ptr<double> pd; double *p_reg = new double; pd = p_reg; // not allowed (implicit conversion) pd = shared_ptr<double>(p_reg); // allowed (explicit conversion) shared_ptr<double> pshared = p_reg; // not allowed (implicit conversion) shared_ptr<double> pshared(p_reg); // allowed (explicit conversion)
2. 智慧指針類的析構函數中的delete,只能用於堆記憶體中動態建立(new)的對象。
string vacation("I wandered lonely as a cloud."); shared_ptr<string> pvac(&vacation); // No
pvac過期時,程式將把delete運算符用於非堆記憶體,這是錯誤的。
3. 為何摒棄auto_ptr?
先來看下面的賦值語句:
auto_ptr< string> ps (new string ("I reigned lonely as a cloud.」); auto_ptr<string> vocation; vocation = ps;
上述賦值語句將完成什麼工作呢?如果ps和vocation是常規指針,則兩個指針將指向同一個string對象。但對於智慧指針來說,這是不能接受的,因為程式將試圖刪除同一個對象兩次——一次是ps過期時,另一次是vocation過期時。要避免這種問題,方法有多種:
- 定義陚值運算符,使之執行深複製。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本,缺點是浪費空間,所以智慧指針都未採用此方案。
- 建立所有權(ownership)概念。對於特定的對象,只能有一個智慧指針可擁有,這樣只有擁有對象的智慧指針的構造函數會刪除該對象。然後讓賦值操作轉讓所有權。這就是用於auto_ptr和unique_ptr 的策略,但unique_ptr的策略更嚴格。
- 創建更智慧的指針,跟蹤引用特定對象的智慧指針數。這稱為引用計數。例如,賦值時,計數將加1,而指針過期時,計數將減1。當減為0時才調用delete。這是shared_ptr採用的策略。
當然,同樣的策略也適用於複製構造函數。
每種方法都有其用途。下面是不適合使用auto_ptr的示例。
#include <iostream> #include <string> #include <memory> using namespace std; int main() { auto_ptr<string> films[5] = { auto_ptr<string> (new string("Fowl Balls")), auto_ptr<string> (new string("Duck Walks")), auto_ptr<string> (new string("Chicken Runs")), auto_ptr<string> (new string("Turkey Errors")), auto_ptr<string> (new string("Goose Eggs")) }; auto_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership. cout << "The nominees for best avian baseballl film are\n"; for(int i = 0; i < 5; ++i) cout << *films[i] << endl; cout << "The winner is " << *pwin << endl; cin.get(); return 0; }
運行下發現程式崩潰了,這裡的問題在於,
pwin = films[2];
所有權從films[2]轉讓給pwin,此時films[2]不再引用該字元串從而變成空指針,下面輸出訪問空指針導致程式崩潰了。如果上述程式碼,
- 使用unique_ptr:編譯出錯,與auto_ptr一樣,unique_ptr也採用所有權模型,但在使用unique_ptr時,程式不會等到運行階段崩潰,而在編譯器因下述程式碼行出現錯誤:
unique_ptr<string> pwin; pwin = films[2]; // films[2] loses ownership.
- 使用shared_ptr:運行正常,因為shared_ptr採用引用計數,pwin和films[2]都指向同一塊記憶體,在釋放空間時因為事先要判斷引用計數值的大小因此不會出現多次刪除一個對象的錯誤。
錯誤的使用auto_ptr可能導致問題(這種程式碼的行為是不確定的,其行為可能隨系統而異)。因此為了避免潛在的程式崩潰,要摒棄auto_ptr。
4. unique_ptr為何優於auto_ptr
- unique_ptr比auto_ptr更安全,編譯階段錯誤比潛在的程式崩潰更安全
- 相比於auto_ptr,unique_ptr還有一個可用於數組的變體。
std::unique_ptr<double> pda(new double(5));//will use delete[]
new/delete | new[]/delete[] | |
auto_ptr | ✔ | |
unique_ptr | ✔ | ✔ |
shared_ptr | ✔ |
- 當程式試圖將一個 unique_ptr 賦值給另一個時,如果源 unique_ptr 是個臨時右值,編譯器允許這麼做;如果源 unique_ptr 將存在一段時間,編譯器將禁止這麼做。
unique_ptr<string> pu1(new string ("hello world")); unique_ptr<string> pu2; pu2 = pu1; // #1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string ("You")); // #2 allowed
其中#1留下懸掛的unique_ptr(pu1),這可能導致危害。而#2不會留下懸掛的unique_ptr,因為它調用 unique_ptr 的構造函數,該構造函數創建的臨時對象在其所有權讓給 pu3 後就會被銷毀。
當然,您可能確實想執行類似於#1的操作,僅當以非智慧的方式使用摒棄的智慧指針時(如解除引用時),這種賦值才不安全。要安全的重用這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你能夠將一個unique_ptr賦給另一個。下面是一個使用前述demo()函數的例子,該函數返回一個unique_ptr<string>對象:
使用move後,原來的指針仍轉讓所有權變成空指針,可以對其重新賦值。
unique_ptr<string> ps1, ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout << *ps2 << *ps1 << endl;
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())。例如,可在程式中使用類似於下面的程式碼段。
unique_ptr<int> make_int(int n) { return unique_ptr<int>(new int(n)); } void show(unique_ptr<int> &p1) { cout << *a << ' '; } int main() { ... vector<unique_ptr<int> > vp(size); for(int i = 0; i < vp.size(); i++) vp[i] = make_int(rand() % 1000); // copy temporary unique_ptr vp.push_back(make_int(rand() % 1000)); // ok because arg is temporary for_each(vp.begin(), vp.end(), show); // use for_each() ... }
其中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>:
unique_ptr<int> pup(make_int(rand() % 1000)); // ok shared_ptr<int> spp(pup); // not allowed, pup as lvalue 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++ Primer Plus》16.2節 智慧指針模板類