轉載:http://www.cnblogs.com/wuchanming/p/4411878.html
1. 介紹
本文介紹智能指針的使用。智能指針是c++ 中管理資源的一種方式,用智能指針管理資源,不必擔心資源泄露,將c++ 程序員 從指針和內存管理中解脫出來,再者,這也是c++發展的趨勢(這話不是我說的,見《Effective c++》和《c++實踐編程》),應該認真學習一下。
智能指針中,最有名的應該數auto_ptr,該智能指針已經被納入標準庫,只需要包含<memory>
頭文件即可以使用,另外,TR1文檔定義的shared_ptr和weak_ptr也已經實現(我用的gcc版本是gcc 4.6.1),它們的頭文件是<tr1/memory>
?。除此之外,還有幾個好用的智能指針,不過它們屬于boost庫,不屬于STL ,所以,用不用得到,根據自己的需要。不過,了解一下總無妨,正所謂"功不唐捐"嘛。
下面分別介紹auto_ptr,scoped_ptr,scoped_array,shared_ptr,shared_array, weak_ptr 和 intrusive_ptr 。
2. auto_ptr
2.1 為什么要用智能指針
在介紹第一個智能指針之前,先介紹下為什么要使用智能指針。先看下面這個函數:
void f()
{ classA* ptr = new classA; // create an object explicitly ... // perform some operations delete ptr; // clean up(destory the object explicitly) }
這個函數是一系列麻煩的根源,一個顯而易見的問題是,我們經常忘了delete 動作,特別是當函數中間存在return 語句時更是如此。然而,真正的麻煩發生于更隱晦之處,那就是當異常發生時,我們所要面對的災難,異常一旦出現,函數將立刻退離,根本不會調用函數尾端的delete 語句。結果可能是內存遺失。防止這種資源遺失的常見辦法就是捕捉所有異常,例如:
void f()
{ classA* ptr = new classA; // create an object explicitly try{ ... // perform some operations } catch(...){ delete ptr; //-clean up throw;//-rethrow the exception } delete ptr; // clean up(destory the object explicitly) }
你看,為了異常發生時處理對象的刪除工作,程序代碼變得多么復雜和累贅!如果還有第二個對象,如果還需要更多的catch 子句,那么簡直是一場惡夢。這不是優良的編程風格,復雜而且容易出錯,必須盡力避免。
如果使用智能指針,情況就會大不相同了。這個智能指針應該保證,無論在何種情形下,只要自己被摧毀,就一定要連帶釋放其所指資源。而由于智能型指針本身就是局部變量,所以無論是正常退出還是異常退出,只要函數退出,它就一定會被銷毀。auto_ptr正是這種只能型指針。
2.2 auto_ptr
auto_ptr 是這樣一種指針:它是"它所指向的對象"的擁有者,所以,當身為對象擁有者的auto_ptr 被摧毀時,該對象也將遭到摧毀。auto_ptr 要求一個對象只能有一個擁有者,嚴禁一物二主。更詳細的說,?auto_ptr 管理的資源必須絕對沒有一個以上的auto_ptr 同時指向它。?這是因為資源的擁有者在銷毀的時候,會銷毀它所擁有的資源,資源不能釋放兩次,如果同時有兩個auto_ptr擁有同一個資源,那么,在第一個auto_ptr銷毀以后,第二個auto_ptr就成為野指針了,所以,任何時刻,一個資源只有一個擁有者。
下面是上例改寫后的版本:
#include <iostream>
#include <memory> using namespace std; void f() { //create and initialize an auto_ptr std::auto_ptr<classA> ptr(new classA); ... //perform some operations }
不需要delete ,也不再需要catch 了。auto_ptr 的接口與一般指針非常相似:operator *用來提取其所指對象,operator-> 用來指向對象中的成員。然而,所有指針算法(包括++在內)都沒有定義。
注意,auto_ptr< >允許你使用一般指針慣用的賦值(assign)初始化方式。必須直接使用數值來完成除始化:
std::auto_ptr<classA> ptr1(new classA); //OK std::auto_ptr<classA> ptr2 = new classA;//ERROR
有了上面兩條語句,那么下面的問題就很顯然了。
std::auto_ptr<classA> ptr; // create an auto_ptr ptr = new classA; // ERROR ptr = std::auto_ptr<classA>(new classA); // ok, delete old object and own new
2.3 auto_ptr 擁有權的轉移
上面提到過,絕對沒有一個以上的auto_ptr同時指向同一個資源,那么,如果你復制(或賦值)一個auto_ptr指針會發生什么呢?發生擁有權轉移,如下所示:
//initizlize an auto_ptr with a new object
std::auto_ptr<classA> ptr1(new classA); //copy the auto_ptr std::auto_ptr<classA> ptr2(ptr1);
在第一個語句中,ptr擁有了那個new 出來的對象。在第二個語句中,擁有權由ptr1 轉移到ptr2,此后,ptr2擁有那個對象,ptr1則是一個空指針。
同理,賦值動作也會發生擁有權的轉移。
//initizlize an auto_ptr with a new object
std::auto_ptr<classA> ptr1(new classA); //copy the auto_ptr std::auto_ptr<classA> ptr2; ptr2 = ptr1;
在上面的語句中,如果ptr2已經擁有一個對象,則,賦值動作發生時,會調用delete,將該對象刪除。
因為auto_ptr 會發生擁有權轉移問題,所以,不能完全像使用普通指針一樣使用auto_ptr ,下面這個錯誤的用法演示的auto_ptr 的特性。
//this is a bad example
template <class T> void bad_print(std::auto_ptr<T> p)//p gets ownership of passed argument { //does p own an object? if (p.get() == NULL) { std::cout << "NULL"; } else { std::cout << *p; } //Oops,exiting delete the object to which p refers } int main(int argc, char const* argv[]) { std::auto_ptr<int> p(new int); *p = 42; // change value to which p refers bad_print(p); // Oops,deletes the memory to which p refers *p = 18; // RUNTIME ERROR return 0; }
我們只是想通過print函數,打印對象的值,可是,卻不小心把對象給銷毀了,這是非常低級的錯誤,再多用幾次auto_ptr 以后,就不會出現這種情況了。如果我們不是通過傳值,而是通過傳遞一個引用會怎么樣呢?可以這么做,可是,你得非常小心,千萬別在調用的函數里面將資源的擁有權轉移了。正確的用法應該聲明指針常量,如下所示:
const std::auto_ptr<int>p(new int); *p = 42 // change value to which p refers bad_print(p); // COMPILE-TIME ERROR *p = 18; // OK
注意,auto_ptr 是一個指針,const auto_ptr 要表達的意思是"指針常量,指針不可指向其他資源,但是指針所指之物可以修改",而不是指向常量的指針。所以,const auto_ptr 類似于?T* const p
而不是指向常量的指針const T* p
?。下面是一個使用auto_ptr 指針的完整示例:
#include <iostream>
#include <memory> using namespace std; //define output operator for auto_ptr //-print object value or NULL template<class T> ostream& operator<< (ostream &strm, const auto_ptr<T> &p) { //does p own an object ? if (p.get() == NULL) { strm << "NULL"; } else { strm << *p; } return strm; } int main(int argc, char* argv[]) { auto_ptr<int> p(new int(42)); auto_ptr<int> q; cout << "after initizlization:" << endl; cout << "p : " << p << endl; cout << "q : " << q << endl; q = p; cout << "after assigning auto pointers:" << endl; cout << "p : " << p << endl; cout << "q : " << q << endl; *q += 13; p = q; cout << "after change and reassignment" << endl; cout << "p : " << p << endl; cout << "q : " << q << endl; return 0; }
輸出結果如下:
after initizlization:
p : 42 q : NULL after assigning auto pointers: p : NULL q : 42 after change and reassignment p : 55 q : NULL
2.4 auto_ptr 注意事項
-
auto_ptr(以及后面介紹的std::tr1::shared_ptr) 在其析構函數內做delete,而不是delete[]動作,那意味著在動態分配而得到的array上使用auto_ptr(或tr1::shared_ptr)是一個餿主意。但是,這樣的代碼是可以通過編譯的,所以需要用戶自己留心。下面的代碼就會出現用new []分配資源,用delete而不是delete[] 釋放資源一樣的問題。
std::auto_ptr<std::string> aps(new std::string[10]);//資源泄漏 std::tr1::shared_ptr<int> spi(new int[1024]); //資源泄漏
-
標準容器需要元素具有可復制和可賦值的特性,而復制和賦值操作會使auto_ptr 發生所有權轉移,所以,auto_ptr 不能存放在容器中。
3. scoped_ptr
有了上面對auto_ptr 的解釋,理解scoped_ptr 就沒有什么難度了。scoped_ptr 的名字向讀者傳遞了明確的信息,這個智能指針只能在本作用域中使用,不希望被轉讓。 scoped_ptr 通過將拷貝構造函數和operator= 函數聲明為私有,以此阻止智能指針的復制,也就關閉了所有權轉移的大門。
scoped_ptr 的用法與auto_ptr 幾乎一樣,大多數情況下它可以與auto_ptr 相互替換,它也可用從一個auto_ptr 獲得指針的管理權(同時,auto_ptr 失去管理權)
scoped_ptr 也具有與auto_ptr 同樣的"缺陷"——不能用作容器的元素,但原因不同,auto_ptr 是因為它的轉移語義,而scoped_ptr 則是因為不支持拷貝和賦值,不符合容器對元素類型的基本要求。
下面的代碼演示了scoped_ptr 與auto_ptr 的區別。
auto_ptr<int> ap(new int(10)); // 一個auto_ptr<int> scoped_ptr<int> sp(ap); // 從auto_ptr 獲得原始指針 assert(ap.get() == 0); // 原auto_ptr 不再擁有指針 ap.reset(new int(20)); // auto_ptr 擁有新的指針 cout << *ap << "," << *sp << endl; auto_ptr<int> ap2; ap2 = ap; // ap2 從ap 獲得原始指針,發生所有權轉移 assert(ap.get() == 0); // ap 不再擁有指針 scoped_ptr<int> sp2; // 另一個scoped_ptr sp2 = sp; // 賦值操作,無法通過編譯
比起auto_ptr ,scoped_ptr 更明確的表達了代碼原始編寫者的意圖:只能在定義的作用域內使用,不可轉讓。
4. scoped_array
scoped_array 與scoped_ptr 沒什么區別,主要區別就是用 new[] 分配資源,用 delete [] 釋放資源,而scoped_ptr 用new 分配資源,用delete 釋放資源。用法如下:
#include <iostream>
#include <algorithm> #include <iterator> #include <boost/smart_ptr.hpp> using namespace std; using namespace boost; int main(int argc, char* argv[]) { int *arr = new int[100]; //動態分配資源 scoped_array<int> sa(arr);//用scoped_array 對象代理原始動態數組 //scoped_array<int> sa( new int[100]); fill_n(&sa[0], 100, 5); sa[10] = sa[20] + sa[30];//像普通數組一樣使用 cout << sa[10] << "\t" << sa[20] << endl; return 0;//在作用域最后,自動釋放資源 }
scoped_array 與 scoped_ptr 接口和功能幾乎一樣,主要區別如下:
- 構造函數接受的指針p 必須是new [] 的結果,而不能是new 表達式的結果。
- 沒有* , -> 操作符重載,因為scoped_array 持有的不是一個普通指針
- 析構函數使用delete []釋放資源,而不是delete
- 提供operator[] 操作符重載,可以像普通數組一樣使用下標訪問元素
- 沒有begin() end() 等類似容器的迭代器
上面這個例子,可以很方便的使用vector代替,《Boost 程序庫開發指南》的作者并不推薦使用scoped_array。
5. shared_ptr
5.1 shared_ptr 介紹
上面已經介紹了3種智能指針,如果按照重要程度排序,auto_ptr 是最重要的,其次應該算shared_ptr 了,shared_ptr 已經被納入標準庫了,用gcc 的用戶只需要#include<tr1/memory>
用visual studio 08/10 的用戶通過加入頭文件#include<memory>
即可。
shared_ptr 是一個最像指針的"智能指針",它實現了引用計數的功能,所以,指針可以隨意復制,在函數間傳遞,或者存儲在容器里面。
shared_ptr 還有兩個特有的成員函數,分別是:
- unique() 用于檢查指針是否唯一的,如果是唯一的,就相當于auto_ptr
- use_count() 返回當前指針的引用計數,use_count() 不提供高效率的操作,所以,use_count() 應該僅僅用于測試或者調試。
下面看看shared_ptr 的用法:
#include <iostream>
#include <tr1/memory> #include <assert.h> using namespace std; int main(int argc, char* argv[]) { std::tr1::shared_ptr<int> sp( new int(10));//一個指向整數的shared_ptr assert( sp.unique());//現在shared_ptr 是指針的唯一持有者 std::tr1::shared_ptr<int> sp2 = sp;//第二個shared_ptr ,拷貝構造函數 //兩個shared_ptr 相等,指向同一個對象,且引用計數為2 assert(sp == sp2 && sp.use_count() == 2); *sp2 = 100;//使用解引用操作符修改被指對象 assert(*sp == 100);//另一個shared_ptr 同時也被修改 sp.reset(); assert(!sp);//sp 不再持有對象 return 0; }
再看一個復雜一點的例子,用以演示智能指針作為成員變量和函數參數的情況。
#include <iostream>
#include <tr1/memory> #include <assert.h> using namespace std; using namespace std::tr1; class shared{ private: shared_ptr<int> p; public: shared(shared_ptr<int> p_):p(p_){}; void print() { cout << "count:" << p.use_count() << " v = " << *p << endl; } }; void print_fun(shared_ptr<int> p) { cout << "count:" << p.use_count() << " v = " << *p << endl; } int main(int argc, char* argv[]) { shared_ptr<int> p(new int(100)); shared s1(p), s2(p); s1.print(); s2.print(); *p = 20; print_fun(p); s1.print(); }
輸出結果如下:
count:3 v = 100 count:3 v = 100 count:4 v = 20 count:3 v = 20
可以看到,我們不用關心shared_ptr 的具體實現,也不需要煩心它的引用計數是多少,我們只需要把它當成一個普通指針使用,再也不用擔心資源泄漏。
auto_ptr 不能一物侍二主,所以,拷貝的時候會發生所有權轉移,而shared_ptr 則不存在這個問題呢,那么,把一個 auto_ptr 復制給 shared_ptr 或者把一個shared_ptr 復制給auto_ptr 會發生什么呢?答案是編譯錯誤,即你不能這么做。
5.2 make_shared
前面說過,shared_ptr 是最像指針的智能指針,有了shared_ptr ,我們幾乎可以拋棄delete了,但是,我們還是用到了new,用到了new 而不delete ,很不對稱不是嗎,所以,TR1又定義了一個小工具make_shared(類似與make_pair)來幫助我們生成對象,不過,這個功能好像還沒有實現,如果,等不及要玩一下,可以用boost庫,make_shared 在頭文件#include<boost/make_shared.hpp>
中定義,使用方法如下:
#include <iostream>
#include <string> #include <vector> #include <boost/make_shared.hpp> using namespace std; int main(int argc, char const* argv[]) { boost::shared_ptr<string> sp = boost::make_shared<string>("make_shared"); cout << *sp << endl; boost::shared_ptr< vector<int> > spv = boost::make_shared< vector<int> >(10, 2); cout << spv->size() << endl; return 0; }
shared_ptr 可以應用于標注庫,唯一需要牢記的是,shared_ptr 是一個指針,行為類似于普通指針,知道這一點以后,下面的代碼就不難理解了。
#include <boost/make_shared.hpp> #include <iostream> #include <vector> #include <boost/smart_ptr.hpp> using namespace std; using namespace boost; int main(int argc, char const* argv[]) { typedef vector< shared_ptr<int> > vs; //聲明一個持有shared_ptr 的標準容器類型,元素被初始化為空指針 vs v(10); int i = 0; for (vs::iterator pos = v.begin(); pos != v.end(); ++pos) { (*pos) = make_shared<int>(++i);//使用工廠函數(make_shared)賦值 cout << *(*pos) << ", ";//輸出值 } cout << endl; shared_ptr<int> p = v[9]; *p = 100; cout << *v[9] << endl; return 0; }
5.3 shared_ptr 的缺陷(循環引用)
shared_ptr 需要當心循環引用的問題,不然還是會發生資源泄漏。詳細信息見這里。
6. shared_array
我們知道scoped_ptr 和 scoped_array 的用法和區別以后,很容易猜到shared_array 的用法了。
shared_array 類似于shared_ptr ,它包裝了new[] 操作符在堆上分配的動態數組,同樣,使用引用計數機制為動態數組提供了一個代理,可以在程序的生命周期里上期存在,直到沒有任何引用后才釋放內存。
shared_array 的接口和功能幾乎與shared_ptr 是相同的,主要區別如下:
- 構造函數接受指針p必須是new[] 的結果,而不是new 分配的資源
- 提供operator[] 操作符重載,可以像普通數組一樣用下標訪問
- 沒有* -> 操作符重載,因為shared_array 持有的不是一個普通指針
- 析構函數使用delete[] 釋放資源,而不是delete
shared_array 用法的簡單示例:
#include <iostream>
#include <boost/smart_ptr.hpp> #include <assert.h> using namespace std; using namespace boost; int main(int argc, char const* argv[]) { shared_array<int> sa( new int[100]); shared_array<int> sa2 = sa; sa[0] = 10; cout << sa.use_count() << endl; cout << sa[0] << endl; assert( sa2[0] == 10); return 0; }
7. weak_ptr
關于weak_ptr 我是知其然,但不知其所以然。下面的說明和例子都來自《Boost 程序庫完全開發指南》,無任何更改,沒有理解透徹,怕改錯了。
weak_ptr 被設計為與shared_ptr 共同工作,可以從一個shared_ptr 或者另一個weak_ptr 對象構造,獲得資源的觀測權。但是weak_ptr 沒有共享資源,它的構造函數不會引起指針引用計數的增加。同樣,在weak_ptr 析構時,也不會導致引用計數減少,它只是一個靜靜的觀察者。
使用weak_ptr 的成員函數use_count() 可以觀測資源的引用計數,另一個成員函數expired() 的功能等價于use_count() == 0
,但更快,表示被觀測的資源已經不復存在。
weak_ptr 沒有重載operator* 和 -> ,這是特意的,因為它不共享指針,不能操作資源,這正是它"弱"的原因,但它可以使用一個非常重要的成員函數lock() 從被觀測的shared_ptr 獲得一個可用的shared_ptr 對象,從而操作資源。但當expired() == ture
的時候,lock()函數返回一個存儲空指針的shared_ptr 。
下面的代碼示范了weak_ptr 的用法:
shared_ptr<int> sp (new int(10)); // 一個shared_ptr assert(sp.use_count() == 1); weak_ptr<int> wp(sp); // 從shared_ptr 創建weak_ptr assert(sp.use_count() == 1); // weak_ptr 不影響引用計數 if (!wp.expired()) // 判斷weak_ptr 觀察的對象是否有效 { shared_ptr<int> sp2 = wp.lock();//獲得一個shared_ptr *sp2 = 100; assert(sp.use_count() == 2); }//退出作用域,sp2 自動析構,引用計數減1 assert(sp.use_count() == 1); sp.reset(); // shared_ptr 失效 assert(wp.expired()); // weak_ptr 將獲得一個空指針 assert(!wp.lock());
8. intrusive_ptr
Boost 庫實現了該指針,Boost 庫不推薦使用intrusive_ptr。
9. 注意事項
-
在資源管理類中提供對原始資源的訪問(Item 15)
使用智能指針的時候,可以通過get()成員函數,獲取原始指針,從而與一下需要用到原始資源的API打交道,如果這樣的API特別多,每次都寫.get() 不光費時,而且不夠清晰,這時,應該提供隱式類型轉換。如下所示:
class Font{ public: .... //隱式類型轉換 operator FondHandle() const {return f;} ... };
-
以獨立的語句將newd 對象置入智能指針(Item 17)
應該用獨立的語句將newd 的對象置入只能指針,考慮如下調用: processWidget(std::tr1::shared_ptr(new Widget), priority());
在上面的調用中,需要處理以下三件事:
- 調用priority
- 執行
new Widget
- 調用
tr1::shared_ptr
構造函數
c++ 編譯器會以什么樣的次序完成上面三件事,我們不得而知,如果調用序列如下:
- 執行
new Widget
- 調用priority
- 調用
tr1::shared_ptr
構造函數
萬一對priority 的調用導致異常,則new Widget
返回的指針就會遺失,我們無法使用,也無法釋放該資源,所以,安全的處理方式,應該是這樣的:
std::tr1::shared_ptr<Widget> pw(new Widget); processWidget(pw, priority());
10. 總結
在上面所有介紹的智能指針中,auto_ptr ,shared_ptr 和weak_ptr 已經納入標準庫,可以放心使用,而不用擔心可移植性的問題。其中auto_ptr 和shared_ptr 最為重要,shared_ptr和普通指針最為相似,不知道該用哪種類型的智能指針的時候,就用shared_ptr 。
參考資料
- 《C++ 標準程序庫》
- 《Boost 程序庫完全開發指南》
- 《Effective c++》
轉載:http://mingxinglai.com/cn/2013/01/smart-ptr/