C++_智能指針模板類
智能指針是行為類似於指針的類對象,但這種對象還有其他功能。
本節介紹三個可幫助管理動態內存分配的智能指針模板(auto_ptr、unique_ptr和shared_ptr)。
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 是C++98提供的解決方案,但被C++11所摒棄;
C++提供了另外兩種解決方案unique_ptr和shared_ptr;
=========================================
一、使用智能指針
這三個智能指針模板(auto_ptr、unique_ptr和shared_ptr)都定義了類似指針的對象,可以將new獲得的地址(直接或間接)賦給這種對象。
當智能指針過期時,其析構函數將使用delete來釋放內存。
因此如果將new返回的地址賦給這些對象,將無需記住稍後釋放這些內存:在智能指針過期時,這些內存將自動釋放。
要創建智能指針對象,必須包含頭文件memory,該文件模板定義。
然後使用通常的模板語法來實例化所需的類型指針。
例如:模板auto_ptr包含如下構造函數:
template <class X> class auto_ptr {
public:
explicit auto_ptr(X*p = 0) throw();
...
};
auto_ptr<double> pd (new double); //pd是auto_ptr to double,代替了double * pd
auto_ptr<string> ps (new string); //ps是auto_ptr to string,代替了 string * ps
其中的new double是new返回的指針,指向新分配的內存塊。它是構造函數auto_ptr<double>的參數;
因此,要轉換remodel()函數,應按照下面3個步驟進行:
1、包含頭文件memory;
2、將指向string的指針替換為指向string的智能指針對象;
3、刪除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名稱空間中,接下來的程序演示了如何使用全部三種指針。
1 //smrtptrs.cpp -- using three kinds of smart pointers 2 //requires support of C++11 shared_ptr and unique_ptr 3 4 #include <iostream> 5 #include <string> 6 #include <memory> 7 8 class Report 9 { 10 private: 11 std::string str; 12 public: 13 Report (const std::string s):str(s) 14 {std::cout<<"Object created!\n";} 15 ~Report() {std::cout<<"Object deleted!\n";} 16 void comment() const {std::cout<<str<<"\n";} 17 }; 18 19 int main() 20 { 21 { 22 std::auto_ptr<Report> ps (new Report("using auto_ptr")); 23 ps -> comment(); //use -> to invoke a member function 24 } 25 { 26 std::shared_ptr<Report> ps (new Report("using shared_ptr")); 27 ps -> comment(); 28 } 29 { 30 std::unique_ptr<Report> ps (new Report("using unique_ptr")); 31 ps->comment(); 32 } 33 return 0; 34 }
所有智能指針類都有一個explicit構造函數,該構造函數將指針作為參數。因此不需要自動將指針轉換為智能指針對象://explicit直言的、不隱瞞的;//implicit不言明的、含蓄的
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呢?首先看下下列語句:
auto_ptr<string> ps (new string("I reigned lonely as cloud."));
auto_ptr<string> vocation;
vocation = ps;
這兩個指針將指向同一個string對象,這是不能接受的。因為程序將試圖刪除同一個對象兩次——一次是ps過期時,另一次是vocation過期時。
要避免這種問題,有很多方法:
1、定義賦值運算符,使之執行深復制。這樣兩個指針將指向不同的對象,其中一個對象是另一個對象的副本。
2、建立所有權(ownership)概念,對特定的對象,只能有一個智能指針可擁有它,這樣只有擁有對象的智能指針的析構函數會刪除該對象。然後,讓賦值操作轉讓所有權。這就是用於auto_ptr和unique_ptr的策略,但unique_ptr的策略更嚴格。
3、創建智能更高的指針,跟蹤引用特定對象的智能指針數。這稱之為引用計數(reference counting)。例如,賦值時,計數將加1,指針過期時,計數將減1。僅當最後一個指針過期時,才調用delete。這是shared_ptr采用的策略。
接下來是一個不適合使用auto_ptr的例子:
1 //fowl.cpp -- auto_ptr a poor choice 2 #include <iostream> 3 #include <string> 4 #include <memory> 5 6 int main() 7 { 8 using namespace std; 9 auto_ptr<string> films[5] = 10 { 11 auto_ptr<string> (new string("Fowl Balls")), 12 auto_ptr<string> (new string("Duck Walks")), 13 auto_ptr<string> (new string("Chicken Runs")), 14 auto_ptr<string> (new string("Goose Eggs")), 15 auto_ptr<string> (new string("Turkey Errors")) 16 }; 17 auto_ptr<string> pwin; 18 pwin = films[2]; 19 20 cout << "The nominees for best avian basketball film are\n"; 21 for (int i = 0; i < 5; i++) 22 cout<<*films[i]<<endl; 23 cout << "The winner is "<< * pwin<<"!\n"; 24 cin.get(); 25 return 0; 26 }
下面是程序的輸出:
The nominees for best avian baseball film are
Fowl Balls
Duck Walks
Segmentation fault(core dumped)
消息core dumped 表明,錯誤地使用auto_ptr可能導致問題。問題在於,下面的語句將所有權從film[2]轉讓給pwin:
pwn = films[2];
當film[2]不再引用該字符串,在auto_ptr放棄對象的所有權後,但有可能會使用它來訪問該對象。
當程序打印film[2]指向的字符串時,卻發現這是一個空指針,這顯然是討厭的意外。
如果程序使用shared_ptr代替auto_ptr,則程序將正常運行。這次pwinhefilms[2]指向同一個對象,而引用計數從1增加到2。
在程序末尾,後聲明的pwin將首先調用其析構函數,該析構函數將引用計數降低到1。
然後,shared_ptr數組的成員被釋放,對film[2]調用析構函數,將引用計數降低到0,並釋放以前分配的空間。
因此使用shared_ptr,可以保證程序正常運行;使用auto_ptr會導致程序在運行階段崩潰。
如果使用unique_ptr,結果將如何呢?與auto_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
p4 = p3; //#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比auto_ptr好用
using namespace std;
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指針,這將導致危害。
語句2不會留下懸掛的unique_ptr指針,因為它調用unique_ptr的構造函數,該構造函數創建的臨時對象在其所有權轉讓給pu3後,會被銷毀。
這種隨情況而異的行為表明,unique_ptr優於auto_ptr。
//下面這段更加晦澀
如果我們確實想執行類型#1的語句那樣的操作的話。
僅當以非智能的方式使用遺棄的智能指針,這種賦值才不安全。
要安全地重用這種指針,可給它賦新值。
C++有一個標準庫函數std::move(),讓您能夠將一個unique_ptr賦給另一個。
using namespace std;
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[]的版本。
=========================================
四、選擇智能指針
應該如何選用只能指針?
如果程序要使用多個指向同一個對象的指針,應選擇share_ptr。
這樣的情況包括:有一個數組,並使用一些輔助指針來標識特定的元素,如最大的元素和最小的元素;
兩個對象都包含指向第三個對象的指針;STL容器包含指針。
很多STL算法都支持賦值和復制操作。這些操作可用於shared_ptr,但不能用於unique_ptr(編譯器發出警告)和auto_ptr(行為不確定)。
如果您的編譯器沒有支持shared_ptr,可使用Boost庫提供的shared_ptr。
如果程序不需要多個指向同一個對象的指針,則可以使用unique_ptr。
如果函數使用new分配內存,並返回指向該內存的指針,將其返回類型聲明為unique_ptr是不錯的選擇。
這樣所有權將轉讓給接受返回值的unique_ptr,而該指針將負責調用delete。
如果編譯器沒有提供unique_ptr,可考慮使用BOOST庫提供的scoped_ptr,它與unique_ptr類似。
C++_智能指針模板類