c++智慧指標--所有的型別的解析
參考文獻:
1. 智慧指標背後的設計思想
1.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
將不被執行,因此將導致記憶體洩露 。
常規解決方案:
- 在
throw exception()
delete ps
; - 不要忘了最後一個
delete ps
;
出現問題:在一個大型的工程中,並不能保證所有的開發人員都能在合適的地方新增delete語句。
1.2 智慧指標的設計思想
仿照本地變數能夠自動從棧記憶體中刪除的思想,對指標設計一個解構函式,該解構函式將在指標過期時自動釋放它指向的記憶體,總結來說就是:將基本型別指標封裝為類物件指標(這個類肯定是個模板,以適應不同基本型別的需求),並在解構函式中編寫delete語句以用來刪除指標指向的記憶體空間。
轉換remodel()
函式的步驟:
- 包含頭義件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;
}
2. C++智慧指標簡單介紹
STL一共給我們提供了四種智慧指標:auto_ptr
unique_ptr
、shared_ptr
和weak_ptr
。
其中:auto_ptr
在C++11中已將其摒棄。
使用注意點:
- 所有的智慧指標類都有一個
explicit
建構函式,以指標作為引數。比如auto_ptr
的類模板原型為:
templet<class T>
class auto_ptr {
explicit auto_ptr(X* p = 0) ;
...
};
因此不能自動將指標轉換為智慧指標物件,必須顯示呼叫:
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)
- 對全部三種智慧指標都應避免的一點:
string vacation("I wandered lonely as a child."); //heap param
shared_ptr<string> pvac(&vacation);//NO!!
pvac
過期時,程式將把delete運算子用於非堆(棧)記憶體,這是錯誤的!
- 使用例項
#include <iostream>
#include <string>
#include <memory>
class report
{
private:
std::string str;
public:
report(const std::string s) : str(s){
std::cout<<"Object created.\n";
}
~report(){
std::cout<<"Object deleted.\n";
}
void comment() const {
std::cout<<str<<"\n";
}
};
int main(){
{
std::auto_ptr<report> ps(new report("using auto ptr"));
ps->comment();
}//auto_ptr 作用域結束
{
std::shared_ptr<report> ps(new report("using shared_ptr"));
ps->comment();
}//shared_ptr 作用域結束
{
std::unique_ptr<report> ps(new report("using unique ptr"));
ps->comment();
}//unique_ptr 作用域結束
return 0;
}
3. 為什麼摒棄auto_ptr
?
問題來源:
auto_ptr<string> ps (new string("I reigned lonely as a cloud."));
auto_ptr<string> vocation;
vocation = ps;
如果ps
和vocation
是常規指標,則兩個指標指向同一個string物件,當指標過期時,則程式會試圖刪除同一個物件,要避免這種問題,解決辦法:
- 定義賦值運算子,使之執行深複製。這樣兩個指標將指向不同的物件,其中的一個物件是另一個物件的副本,缺點是浪費空間,所以智慧指標都未採取此方案。
- 建立所有權(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 owership,將所有權從films[2]轉讓給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;
}
執行下發現程式崩潰了,原因是films[2]已經是空指標了,輸出空指標就會崩潰。如果把auto_ptr
換成shared_ptr
或unique_ptr
後,程式就不會崩潰,原因如下:
-
適用
shared_ptr
時執行正常,因為shared_ptr
採用引用計數,pwin
和films[2]
都指向同一塊記憶體,在釋放空間時因為事先要判斷引用計數值的大小,因此不會出現多次刪除一個物件的錯誤。 -
適用
unique_ptr
時編譯出錯,與auto_ptr
一樣,unique_ptr
也採用所有權模型,但在適用unique_ptr
時,程式不會等到執行階段崩潰,在編譯階段下屬程式碼就會出現錯誤:
unique_ptr<string> pwin;
pwin = films[2];//films[2] loses ownership
這就是為何摒棄auto_ptr
的原因:避免潛在的記憶體洩漏問題。
4.unique_ptr
為何優於auto_ptr
?
4.1 使用規則更嚴格
auto_ptr<string> p1(new string("auto")); //#1
auto_ptr<string> p2; //#2
p2 = p1; //#3
在語句#3中,p2
接管string物件的所有權後,p1的所有權將被剝奪。–>可防止p1和p2的解構函式試圖刪除同一個物件。但如果隨後試圖使用p1,則會出現錯誤。
unique_ptr<string> p3(new string("auto"));//#4
unique_ptr<string> p4;//#5
p4=p3;//#6
編譯器會認為#6語句為非法,可以避免上述問題。
4.2 對懸掛指標的操作更智慧
總體來說:允許臨時懸掛指標的賦值,禁止其他情況的出現。
示例:函式定義如下:
unique_ptr<string> demo(const char *s){
unique_ptr<string> temp (new string(a));
return temp;
}
在程式中呼叫函式:
unique_ptr<string> ps;
ps = demo("unique special");
編譯器允許此種賦值方式。總之:當程式試圖將一個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的操作,僅當以非智慧的方式使用摒棄的智慧指標時(如解除引用時),這種賦值才不安全。要安全的重用這種指標,可給它賦新值。C++有一個標準庫函式std::move()
,可以將原來的指標轉讓所有權變成空指標,可以對其重新賦值。
unque_ptr<string> ps1,ps2;
ps1 = demo("hello");
ps2 = move(ps1);
ps1 = demo("alexia");
cout<<*ps2<<*ps1<<endl;
5. 如何選擇智慧指標
使用指南:
- 如果程式要使用多個指向同一個物件的指標,應選用
shared_ptr
。這樣的情況包括:- 有一個指標陣列,並使用一些輔助指標來標示特定的元素,如最大的元素和最小的元素;
- 連個物件包含指向第三個物件的指標;
- STL容器包含指標。很多STL演算法都支援複製和賦值操作,這些操作可用於
shared_ptr
,但不能用於unique_ptr
(編譯器發出warning)和auto_ptr
(行為不確定)。如果你的編譯器沒有提供shared_ptr
,可使用Boost庫提供的shared_ptr
。
- 如果程式不需要多個指向同一個物件的指標,則可使用
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
初始化p1
,而這是不允許的。前面說過,編譯器將發現錯誤使用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
類似。
6. 弱引用智慧指標 weak_ptr
設計weak_ptr
的原因:解決使用shared_ptr
因迴圈引用而不能釋放資源的問題。
6.1 空懸指標問題
有兩個指標p1
和p2
,指向堆上的同一個物件Object,p1
和p2
位於不同的執行緒中。假設執行緒A通過p1
指標將物件銷燬了(儘管把p1
置為NULL),那p2
就成了空懸指標。這是一種典型的C/C++記憶體錯誤。
使用weak_ptr
能夠幫助我們輕鬆解決上述的空懸指標問題(直接使用shared_ptr
也是可以的)。
weak_ptr
不控制物件的生命期,但是它知道物件是否還活著,如果物件還活著,那麼它可以提升為有效的shared_ptr
(提升操作通過lock()
函式獲取所管理物件的強引用指標);如果物件已經死了,提升會失敗,返回一個空的shared_ptr
。
舉個栗子 :
#include <iostream>
#include <memory>
int main()
{
// OLD, problem with dangling pointer
// PROBLEM: ref will point to undefined data!
int* ptr = new int(10);
int* ref = ptr;
delete ptr;
// NEW
// SOLUTION: check expired() or lock() to determine if pointer is valid
// empty definition
std::shared_ptr<int> sptr;
// takes ownership of pointer
sptr.reset(new int);
*sptr = 10;
// get pointer to data without taking ownership
std::weak_ptr<int> weak1 = sptr;
// deletes managed object, acquires new pointer
sptr.reset(new int);
*sptr = 5;
// get pointer to new data without taking ownership
std::weak_ptr<int> weak2 = sptr;
// weak1 is expired!
if(auto tmp = weak1.lock())
std::cout << *tmp << '\n';
else
std::cout << "weak1 is expired\n";
// weak2 points to new data (5)
if(auto tmp = weak2.lock())
std::cout << *tmp << '\n';
else
std::cout << "weak2 is expired\n";
}
6.2 迴圈引用問題
栗子 大法:
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;
class BB;
class AA
{
public:
AA() { cout << "AA::AA() called" << endl; }
~AA() { cout << "AA::~AA() called" << endl; }
shared_ptr<BB> m_bb_ptr; //!
};
class BB
{
public:
BB() { cout << "BB::BB() called" << endl; }
~BB() { cout << "BB::~BB() called" << endl; }
shared_ptr<AA> m_aa_ptr; //!
};
int main()
{
shared_ptr<AA> ptr_a (new AA);
shared_ptr<BB> ptr_b ( new BB);
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
//下面兩句導致了AA與BB的迴圈引用,結果就是AA和BB物件都不會析構
ptr_a->m_bb_ptr = ptr_b;
ptr_b->m_aa_ptr = ptr_a;
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}
執行結果:
可以看到由於AA和BB內部的shared_ptr
各自儲存了對方的一次引用,所以導致了ptr_a
和ptr_b
銷燬的時候都認為內部儲存的指標計數沒有變成0,所以AA和BB的解構函式不會被呼叫。解決方法就是把一個shared_ptr
替換成weak_ptr
。
#include <iostream>
#include <boost/smart_ptr.hpp>
using namespace std;
using namespace boost;
class BB;
class AA
{
public:
AA() { cout << "AA::AA() called" << endl; }
~AA() { cout << "AA::~AA() called" << endl; }
weak_ptr<BB> m_bb_ptr; //!
};
class BB
{
public:
BB() { cout << "BB::BB() called" << endl; }
~BB() { cout << "BB::~BB() called" << endl; }
shared_ptr<AA> m_aa_ptr; //!
};
int main()
{
shared_ptr<AA> ptr_a (new AA);
shared_ptr<BB> ptr_b ( new BB);
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
//下面兩句導致了AA與BB的迴圈引用,結果就是AA和BB物件都不會析構
ptr_a->m_bb_ptr = ptr_b;
ptr_b->m_aa_ptr = ptr_a;
cout << "ptr_a use_count: " << ptr_a.use_count() << endl;
cout << "ptr_b use_count: " << ptr_b.use_count() << endl;
}
執行結果:
最後值得一提的是,雖然通過弱引用指標可以有效的解除迴圈引用,但這種方式必須在能預見會出現迴圈引用的情況下才能使用,即這個僅僅是一種編譯期的解決方案,如果程式在執行過程中出現了迴圈引用,還是會造成記憶體洩漏的。因此,不要認為只要使用了智慧指標便能杜絕記憶體洩漏。