C++智能指針
智能指針模板類
智能指針是行為類似於指針的類對象,但這種對象還有其他功能。本文章介紹三個可幫助管理動態內存分配的只能指針模板。先來看需要哪些功能以及這些功能是如何實現的。請看下面的函數
void remodel(std::string & str) { std::string *ps = new std::string(str); ... str = ps; return ; } |
你可能發現了其中的缺陷。每當調用時,該函數都分配堆中的內存,,但從不回收,從而導致內存泄露。你可能也知道解決之道------只要別忘了在return語句前添加下面的語句,以釋放分配的內存即可:
delete ps;
然而,但凡涉及“別忘了”的解決方法,很少是最佳的。因為你有時可能忘了,有時可能記住了,但可能在不經意間刪除或註釋了這些代碼。即使確實沒有忘記,也可能有問題。請看下面的變體:
void remodel(std::string & str) { std::string *ps = new std::string(str); ... if (weird_thing()) { throw exception(); } str = *ps; delete ps; return; } |
當出現異常時,delete將不被執行,因此也將導致內存泄露。
當remodel()這樣的函數終止(不管是正常終止,還是由於出現了異常而終止),本地變量都將從棧內存中刪除------因此指針ps占據的內存將被釋放。如果ps指向的內存也被釋放,那該有多好啊。如果ps有一個析構函數,該析構函數在ps過期時釋放他指向的內存。因此,ps的問題在於,它只是一個常規指針,不是有析構函數的類對象。如果它是對象,則可以在對象過期時,讓它的析構函數刪除指向的內存。這正是auto_ptr、unique_ptr、shared_ptr背後的思想。模板auto_ptr是C++98提供的解決方案,C++已將其摒棄,並提供了另外兩種解決方案。然而,雖然auto_ptr被摒棄,但它已使用了多年同時,如果您的編譯器不支持其他兩種解決方案,auto_ptr將是唯一的選擇。
使用智能指針
這三個智能指針模板(auto_ptr,unique_ptr,shared_ptr)都定義了類似指針的對象,可以將new獲得(直接或間接)的地址賦給這個對象。當智能指針過期時,其析構函數將使用delete來釋放內存。因此,如果將new返回的地址賦給這些對象,將無需記住稍後釋放這些內存;在智能指針過期時,這些內存將自動被釋放。下圖說明了auto_ptr和常規指針在行為方面的差別;shared_ptr和unique_ptr的行為與auto_ptr相同。
要創建智能指針對象,必須包含頭文件memory,該文件模板定義。然後使用通常的模板語法來實例化所需類型的指針。例如,模板auto_ptr包含如下構造函數:
template<class X> class auto_ptr { explicit auto_ptr(X *p = 0)throw(); ...
}; |
throw()意味這構造函數不會引發異常:與auto_ptr一樣,throw()也被摒棄。因此,請求X類型的auto_ptr將獲得一個指向X類型的auto_ptr:
auto_ptr<double>pd(new double);//pd an auto_ptr to double //(use in place of double *pd) auto_ptr<string>ps(new string);//ps an auto_ptr to string //(use in place of string *ps) |
new double是new返回的指針,指向新分配的內存塊。它是構造函數auto_ptr<double>的參數,即對應於原型中形參p的實參。同樣,new string也是構造函數的實參。其他兩種智能指針使用同樣的語法:
unique_ptr<double> pdu(new double);//pdu an unique_ptr to double shared_ptr<string> pss(new string);//pss a shared_ptr to string |
因此。要轉換remodel()函數,應按下面3個步驟進行:
- 包含頭文件memory;
- 將指向string的指針替換為指向string的智能指針對象;
- 刪除delete語句。
下面是使用auto_ptr修改該函數的結果:
#include <memory>
void remodel(std::string & str)
{
std::auto_ptr<std::string>ps (new std::string(str));
…
if (weird_thing())
{
throw exception();
}
str = *ps;
//delete ps; NO LONGER NEEDED
return ;
}
註意到智能指針模板位於命名空間std中。下面是一個簡單的程序,演示了如何使用全部三種智能指針。每個智能指針都放在一個代碼塊內,這樣離開代碼塊時,指針將過期。Report類使用方法報告對象的創建和銷毀。
//smrtptrs.cpp -- using three kinds of smart pointers //requires support of C++11 shared_ptr and unique_ptr #include <iostream> #include <string> #include <memory>
using namespace std;
class Report { public: Report(const string s) : str(s) { cout << "Object created!" << endl; } ~Report() { cout << "Object deleted!" << endl; } void comment() const { cout << str << endl; } private: string str; };
int main() { { auto_ptr<Report> ps(new Report("using auto_ptr")); ps->comment(); } { shared_ptr<Report> ps(new Report("using shared_ptr")); ps->comment(); } { unique_ptr<Report> ps(new Report("using unique_ptr")); ps->comment(); }
system("pause"); return 0; } |
程序輸出結果:
所有智能指針類都一個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)
由於智能指針模板類的定義方式,智能指針對象的很多方面都類似於常規指針。例如,如果ps是一個智能指針對象,則可以對它執行解除引用操作(*ps)、用它來訪問結構成員(ps->puffIndex)、將他賦給指向相同類型的常規指針,還可以將智能指針對象賦給另一個同類型智能指針對象,但將引起一個問題,這將在下面進行討論。
但在此之前,先說說對全部三種智能指針都應避免的一點:
string vacation(“I wandered lonely as a cloud”);
shared_ptr<string> pvac(&vacation);//NO;
pvac過期時,程序將把delete運算符用於非堆內存,這是錯誤的。
有關智能指針的註意事項
先來看下面的賦值語句:
auto_ptr<string> ps(new string(“I reigned lonely as a cloud”));
auto_ptr<string> vocation;
vocation = ps;
上述賦值語句將完成什麽工作呢?如果ps和vocation是常規指針,則兩個指針將指向同一個string對象這是不能接受的,因為程序將試圖刪除同一個同對象兩次——一次是ps過期時,一次vocation過期時。要避免這種問題,方法有多種。
l 定義賦值運算符,使之執行深復制。這樣兩個指針將指向不同的對象,其中的一個對象是另一個對象的副本。
l 建立所有權(ownership)概念,對於特定的對象,只能有一個智能指針可擁有它,這樣只有擁有和unique_ptr的策略,但unique_ptr的策略更嚴格。
l 創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱為引用計數(reference counting)。這是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 Run")), auto_ptr<string> (new string("Turkey Errors")), auto_ptr<string> (new string("Goose Eggs")) };
auto_ptr<string> pwin; pwin = films[2];
cout << "The nominees for best avian baseball film are \n"; for (int i = 0; i < 5; i++) { cout << *films[i] << endl; } cout << "The winner is " << *pwin << endl; cin.get();
return 0; } |
該程序的輸出:
這裏的問題在於,下面的語句所有權從film[2]轉讓給pwin:
pwin = films[2];//films[2] loses ownership
這導致films[2]不再引用該字符串。在auto_ptr放棄對象的所有權後,便可能使用它來訪問該對象。當程序打印films[2]指向的字符串時,卻發現這是一個空指針,這顯然討厭的意外。
如果在上面程序中使用shared_ptr代替auto_ptr,則程序將正常運行,其輸出如下:
這次pwin和films[2]指向同一個對象,而引用計數從1增加到2。在程序末尾,後申明的pwin首先調用其析構函數,該析構函數將引用技術降低到1.然後,shared_ptr數組的成員被釋放,對filmsp[2]調用析構函數時,將引用計數降低到0,並釋放以前分配的空間。
如果使用unique_ptr結果會怎麽樣呢?unique_ptr也采用所有權模型,但使用unique_ptr時。程序不會等到運行階段崩潰,而是出現語法錯誤,如下
pwin = films[2];
unique_ptr為何優於auto_ptr
請看下面的語句:
auto_ptr<string> p1(new string("auto"));//#1 auto_ptr<string> p2;//#2 p2 = p1;//#3 |
在語句#3中,p2接管string對象的所有權後,p1的所有權被剝奪。前面說過,這是件好事,可防止p1和p2的析構函數試圖刪除同一個對象;但如果程序隨後試圖使用p1,這將是件壞事,因為p1不再指向有效的數據。
下面來看使用unique_ptr的情況:
unique_ptr<string> p3(new string("auto"));//#4 unique_ptr<string> p4;//#5 p3 = p4;//#6 |
編譯器認為語句#6非法,避免了p3不再指向有效數據的問題。因此,unique_ptr比auto_ptr更安全(編譯階段錯誤比潛在的程序崩潰更安全)。
但有時候,有一個智能指針賦給另一個並不會留下危險的懸掛指針。假設有如下函數定義:
unique_ptr<string> demo(const char *s) { unique_ptr<string> temp(new string(s)); return temp; } |
並假設編寫了如下代碼:
unique_ptr<string> ps; ps = demo("Uniquely special"); |
demo()返回一個臨時unique_ptr,然後ps接管了原本歸返回的unique_ptr所有的對象,而返回的unique_ptr被銷毀。這沒有問題,因為ps擁有了string對象的所有權。但這裏的另一個好處是,demo()返回的臨時unique_ptr很快被銷毀,沒有機會使用它來訪問無效的數據。換句話說,沒有理由禁止這種賦值。神奇的是,編譯器確實允許這種賦值!
總之,程序試圖講一個unique_ptr賦給另一個時,如果源unique_ptr是一個臨時右值,編譯器允許這樣做;如果源unique_ptr將存在一段時間,編譯器將禁止這樣做:
unique_ptr<string> pu1(new string("Hi ho!")); unique_ptr<string> pu2; pu2 = pu1; //#1 not allowed unique_ptr<string> pu3; pu3 = unique_ptr<string>(new string("Yo!")); //#2 allowed |
語句#1將留下懸掛的unique_ptr(pu1),這可能導致危害。語句#2不會留下的unique_ptr,因為它調用unique_ptr的構造函數,該構造函數創建的臨時對象在其所有權轉讓給pu3後就會被銷毀。這種隋情況而異的行為表明,unique_ptr優於允許兩種賦值的auto_ptr。這也是禁止(只是一個建議,編譯器並不禁止)在容器對象中使用auto_ptr,但允許使用unique_ptr的原因。如果容器算法試圖對包含unique_ptr的容器執行類似於語句#1的操作,將導致編譯錯誤;如果算法試圖執行類似於語句#2的操作,則不會有任何問題,而對於auto_ptr,類似於語句#1的操作可能導致不確定的行為和神秘的崩潰。
當然,你可能確實想執行類似於語句#1的操作。僅當以非智能的方式使用遺棄的智能指針(如解除引用時),這種賦值不安全。要安全重入這種指針,可給它賦新值。C++有一個標準庫函數std::move(),讓你能能夠將一個unique_ptr賦給另一個。下面是一個使用前述demo()函數的例子,該函數返回一個unique_ptr<string>對象:
unique_ptr<string> ps1, ps2; ps1 = demo("Uniquely special"); ps2 = move(ps1); ps1 = demo(" and more"); cout << *ps2 << *ps1 << endl; |
相比於auto_ptr,unique_ptr還有一個另一個優點。它有一個可用於數組的變體。別忘了,必須將delete和new配對,將delete []和new []配對。模板auto_ptr使用delete而不是delete[],因此只能與new一起使用,而不能與new[]一起使用。但unique_ptr有使用new []和delete []的版本:
unique_ptr<double []>pda(new double(5));//will use delete []
警告:使用new分配內存時,才能使用auto_ptr和shared_ptr,使用new []分配內存時,不能使用它們,不使用new分配內存時,不能使用auto_ptr或shared_ptr;不適用new []分配內存時,不能使用unique_ptr。
C++智能指針