C++中智能指针详解
1、問題引入
? ? ? ?在C++中,靜態內存和棧內存外,還有一部分內存稱為堆程序用堆來存儲動態分配的對象即那些在程序運行時分配的對象,當動態對象不再使用時,我們的代碼必須顯式的銷毀它們。在C++中一般使用“new”:在動態內存中為對象分配一塊空間并返回一個指向該對象的指針,“delete”:指向一個動態獨享的指針,銷毀對象,并釋放與之關聯的內存。
? ? ? ?動態內存管理經常會出現兩種問題:一種是忘記釋放內存,會造成內存泄漏;一種是尚有指針引用內存的情況下就釋放了它,就會產生引用非法內存的指針。
? ? ? ?為了更加容易(更加安全)的使用動態內存,引入了智能指針的概念。智能指針的行為類似常規指針,重要的區別是它負責自動釋放所指向的對象。C++中常用的智能指針有shared_ptr(多個指針指向同一對象)、unique_ptr(獨占所指的對象)、weak_ptr(伴隨類,弱引用)、auto_ptr(局部指針C++11已棄用),位于頭文件memory中。實際上智能指針還有boost::scoped_ptr()、boost::scoped_array、boost::shared_array等。
?
2、shared_ptr類
? ? ? ?shared_ptr允許多個指針指向同一對象,資源可以被多個指針所共享。創建智能指針時必須提供額外的信息,指針可以指向的類型:
shared_ptr<string> p1; shared_ptr<list<int>> p2;? ? ? ?默認初始化的智能指針中保存著一個空指針。?智能指針的使用方式和普通指針類似,解引用一個智能指針返回它指向的對象,在一個條件判斷中使用智能指針就是檢測它是不是空。
if(p1 && p1->empty()){*p1 = "hi"; // 如果p1指向一個空string,解引用p1,將一個新值賦予string }(1)shared_ptr的操作
如下表所示是shared_ptr和unique_ptr都支持的操作:?
如下表所示是shared_ptr特有的操作:
注:make_shared函數:
? ? ? ?最安全的分配和使用動態內存的方法就是調用一個名為make_shared的標準庫函數,此函數在動態內存中分配一個對象并初始化它,返回指向此對象的shared_ptr。頭文件和share_ptr相同,在memory中必須指定想要創建對象的類型,定義格式見下面例子:
shared_ptr<int> p3 = make_shared<int>(42); shared_ptr<string> p4 = make_shared<string>(10,'9'); shared_ptr<int> p5 = make_shared<int>();? ? ? ?make_shared用其參數來構造給定類型的對象,如果我們不傳遞任何參數,對象就會進行值初始化。
(2)shared_ptr的拷貝和賦值?
? ? ? ?當進行拷貝和賦值時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的對象。
auto p = make_shared<int>(42); auto q(p);? ? ? ?可以認為每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數,無論何時我們拷貝一個shared_ptr,計數器都會遞增。當我們給shared_ptr賦予一個新值或是shared_ptr被銷毀(例如一個局部的shared_ptr離開其作用域)時,計數器就會遞減,一旦一個shared_ptr的計數器變為0,它就會自動釋放自己所管理的對象。
auto r = make_shared<int>(42);//r指向的int只有一個引用者 r=q;//給r賦值,令它指向另一個地址//遞增q指向的對象的引用計數//遞減r原來指向的對象的引用計數//r原來指向的對象已沒有引用者,會自動釋放總結:引用計數
增:
- 拷貝:拷貝一個shared_ptr;
- 初始化:用一個shared_ptr初始化另一個shared_ptr;
- 參數:作為參數傳遞給一個函數
- 返回值:作為函數返回值
減:
- 賦新值:給shared_ptr賦予一個新值
- 銷毀:shared_ptr被銷毀后
(3)shared_ptr自動銷毀所管理的對象并自動釋放相關聯的內存
? ? ? ?當指向一個對象的最后一個shared_ptr被銷毀時,shared_ptr類會自動銷毀此對象,它是通過另一個特殊的成員函數-析構函數完成銷毀工作的,類似于構造函數,每個類都有一個析構函數。析構函數控制對象銷毀時做什么操作。析構函數一般用來釋放對象所分配的資源。shared_ptr的析構函數會遞減它所指向的對象的引用計數。如果引用計數變為0,shared_ptr的析構函數就會銷毀對象,并釋放它所占用的內存。
? ? ? ?當動態對象不再被使用時,shared_ptr類還會自動釋放動態對象,這一特性使得動態內存的使用變得非常容易。如果你將shared_ptr存放于一個容器中,而后不再需要全部元素,而只使用其中一部分,要記得用erase刪除不再需要的那些元素。
注:delete之后重置指針值
? ? ? ?在使用new申請了動態內存后,在使用delete之后,指針就變成了空懸指針,即指向一塊曾經保存數據對象但現在已經無效的內存的地址。有一種方法可以避免懸空指針的問題:在指針即將要離開其作用于之前釋放掉它所關聯的內存 如果我們需要保留指針可以在delete之后將nullptr賦予指針,這樣就清楚的指出指針不指向任何對象。
(4)shared_ptr和new結合使用
? ? ? ?如果我們不初始化一個智能指針,它就會被初始化成一個空指針,接受指針參數的職能指針是explicit的,因此我們不能將一個內置指針隱式轉換為一個智能指針,必須直接初始化形式來初始化一個智能指針。
shared_ptr<int> p1 = new int(1024);//錯誤:必須使用直接初始化形式 shared_ptr<int> p2(new int(1024));//正確:使用了直接初始化形式注:默認情況下,一個用來初始化智能指針的普通指針必須指向動態內存,因為智能指針默認使用delete是否它所關聯的內存。
下表為定義和改變shared_ptr的其他方法:
(5)不要混合使用普通指針和智能指針
? ? ? ?如果混合使用的話,智能指針自動釋放之后,普通指針有時就會變成懸空指針,當將一個shared_ptr綁定到一個普通指針時,我們就將內存的管理責任交給了這個shared_ptr。一旦這樣做了,我們就不應該再使用內置指針來訪問shared_ptr所指向的內存了。也不要使用get初始化另一個智能指針或為智能指針賦值。
shared_ptr<int> p(new int(42));//引用計數為1 int *q = p.get();//正確:但使用q時要注意,不要讓它管理的指針被釋放 {//新程序塊//未定義:兩個獨立的share_ptr指向相同的內存shared_ptr(q);}//程序塊結束,q被銷毀,它指向的內存被釋放 int foo = *p;//未定義,p指向的內存已經被釋放了? ? ? ?p和q指向相同的一塊內部,由于是相互獨立創建,因此各自的引用計數都是1,當q所在的程序塊結束時,q被銷毀,這會導致q指向的內存被釋放,p這時候就變成一個空懸指針,再次使用時,將發生未定義的行為,當p被銷毀時,這塊空間會被二次delete。
(6)其他shared_ptr操作
? ? ? ?可以使用reset來將一個新的指針賦予一個shared_ptr:
p = new int(1024);//錯誤:不能將一個指針賦予shared_ptr p.reset(new int(1024));//正確。p指向一個新對象? ? ? ?與賦值類似,reset會更新引用計數,如果需要的話,會釋放p的對象。reset成員經常和unique一起使用,來控制多個shared_ptr共享的對象。在改變底層對象之前,我們檢查自己是否是當前對象僅有的用戶。如果不是,在改變之前要制作一份新的拷貝:
if(!p.unique()){p.reset(new string(*p));//我們不是唯一用戶,分配新的拷貝*p+=newVal;//現在我們知道自己是唯一的用戶,可以改變對象的值 }(6)智能指針陷阱:
- 不使用相同的內置指針值初始化(或reset)多個智能指針。
- 不delete get()返回的指針
- 不使用get()初始化或reset另一個智能指針
- 如果你使用get()返回的指針,記住當最后一個對應的智能指針銷毀后,你的指針就變為無效了
- 如果你使用智能指針管理的資源不是new分配的內存,記住傳遞給它一個刪除器
?
3、unique_ptr
? ? ? ?某個時刻只能有一個unique_ptr指向一個給定對象,由于一個unique_ptr擁有它指向的對象,因此unique_ptr不支持普通的拷貝或賦值操作。?
例:
unique_ptr<string> p1(new string(“hello”)); unique_ptr<string> p2(p1); // 錯誤:unique_ptr不支持拷貝 unique_ptr<string> p3; p3 = p2; // 錯誤:unique_ptr不支持賦值
下表是unique的操作:
注意u.release()和u.reset()的區別:
(1)u.release()是釋放u的對象,但是u所指的對象還存在在內存中,并未被釋放,需要用delete來釋放內存。
(2)u.reset()是是否了u所指的對象。
例:
unique_ptr<string> p1(new string(“hello”)); auto p = p1.release(); delete p; // 用release()后需要用delete來釋放雖然我們不能拷貝或者賦值unique_ptr,但是可以通過調用release或reset將指針所有權從一個(非const)unique_ptr轉移給另一個unique:
例:
//將所有權從p1(指向string Stegosaurus)轉移給p2 unique_ptr<string> p2(p1.release());//release將p1置為空 unique_ptr<string>p3(new string("Trex")); //將所有權從p3轉移到p2 p2.reset(p3.release());//reset釋放了p2原來指向的內存- release成員返回unique_ptr當前保存的指針并將其置為空。因此,p2被初始化為p1原來保存的指針,而p1被置為空。
- reset成員接受一個可選的指針參數,令unique_ptr重新指向給定的指針。
- 調用release會切斷unique_ptr和它原來管理的的對象間的聯系。release返回的指針通常被用來初始化另一個智能指針或給另一個智能指針賦值。
注:不能拷貝unique_ptr有一個例外:
我們可以拷貝或賦值一個將要被銷毀的unique_ptr.最常見的例子是從函數返回一個unique_ptr和返回一個局部對象的拷貝:
例:從函數返回一個unique_ptr
unique_ptr<int> clone(int p) {//正確:從int*創建一個unique_ptr<int>return unique_ptr<int>(new int(p)); }例:返回一個局部對象的拷貝
?
注:
- 向后兼容:auto_ptr :標準庫的較早版本包含了一個名為auto_ptr的類,它具有uniqued_ptr的部分特性,但不是全部。
- 用unique_ptr傳遞刪除器:unique_ptr默認使用delete釋放它指向的對象,我們可以重載一個unique_ptr中默認的刪除器。我們必須在尖括號中unique_ptr指向類型之后提供刪除器類型。在創建或reset一個這種unique_ptr類型的對象時,必須提供一個指定類型的可調用對象刪除器。
?
4、weak_ptr
? ? ? ?weak_ptr是一種不控制所指向對象生存期的智能指針,它指向一個由shared_ptr管理的對象,將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。一旦最后一個指向對象的shared_ptr被銷毀,對象就會被釋放,即使有weak_ptr指向對象,對象還是會被釋放。
? ? ? ? weak_ptr是指向shared_ptr的對象,然而shared_ptr也可以用shared_ptr來接收,那么為什么還要用weak_ptr來接收呢?實際上,使用weak_ptr可以防止用戶訪問一個不再存在的對象,同時使用weak_ptr一般意味著weak_ptr所指的對象可能會被銷毀。
weak_ptr的操作:
? ? ? ?由于對象可能不存在,我們不能使用weak_ptr直接訪問對象,而必須調用lock,此函數檢查weak_ptr指向的對象是否存在。如果存在,lock返回一個指向共享對象的shared_ptr,如果不存在,lock將返回一個空指針。
例:
if(shared_ptr<int> np = wp.lock()) { // 若np不為空則條件成立// 在if中,np和p共享對象 }?
5、auto_ptr
? ? ? ?auto_ptr 是C++標準庫提供的類模板,auto_ptr對象通過初始化指向由new創建的動態內存,它是這塊內存的擁有者,一塊內存不能同時被分給兩個擁有者。當auto_ptr對象生命周期結束時,其析構函數會將auto_ptr對象擁有的動態內存自動釋放。即使發生異常,通過異常的棧展開過程也能將動態內存釋放。auto_ptr不支持new 數組。
(1)初始化auto_ptr
1) 構造函數
1] 將已存在的指向動態內存的普通指針作為參數來構造
int* p = new int(33); auto_ptr<int> api(p);2] 直接構造智能指針
auto_ptr< int > api( new int( 33 ) );2) 拷貝構造
利用已經存在的智能指針來構造新的智能指針:
auto_ptr< string > pstr_auto( new string( "Brontosaurus" ) ); auto_ptr< string > pstr_auto2( pstr_auto );??//利用pstr_auto來構造pstr_auto2? ? ? ?因為一塊動態內存只能由一個智能指針獨享,所以在拷貝構造或賦值時都會發生擁有權轉移的過程。在此拷貝構造過程中,pstr_auto將失去對字符串內存的所有權,而pstr_auto2將其獲得。對象銷毀時,pstr_auto2負責內存的自動銷毀。
3) 賦值
? ? ? ?利用已經存在的智能指針來構造新的智能指針
auto_ptr< int > p1( new int( 1024 ) ); auto_ptr< int > p2( new int( 2048 ) ); p1 = p2;? ? ? ?在賦值之前,由p1 指向的對象被刪除。賦值之后,p1 擁有int 型對象的所有權。該對象值為2048。p2不再被用來指向該對象。
4)直接定義空的auto_ptr
? ? ? ?通常的指針在定義的時候若不指向任何對象,我們用Null給其賦值。對于智能指針,因為構造函數有默認值0,我們可以直接定義空的auto_ptr如下:
auto_ptr< int > p_auto_int;??//不指向任何對象5)防止兩個auto_ptr對象擁有同一個對象(一塊內存)
因為auto_ptr的所有權獨有,所以下面的代碼會造成混亂。
int* p = new int(0); auto_ptr<int> ap1(p); auto_ptr<int> ap2(p);? ? ? ?因為ap1與ap2都認為指針p是歸它管的,在析構時都試圖刪除p, 兩次刪除同一個對象的行為在C++標準中是未定義的。所以我們必須防止這樣使用auto_ptr。
6)警惕智能指針作為參數!
? ? ? ?a、按值傳遞時,函數調用過程中在函數的作用域中會產生一個局部對象來接收傳入的auto_ptr(拷貝構造),這樣,傳入的實參auto_ptr就失去了其對原對象的所有權,而該對象會在函數退出時被局部auto_ptr刪除。如下例:
void f(auto_ptr<int> ap) {cout<<*ap;} auto_ptr<int> ap1(new int(0)); f(ap1); cout<<*ap1; //錯誤,經過f(ap1)函數調用,ap1已經不再擁有任何對象了。? ? ? ?b、引用或指針時,不會存在上面的拷貝過程。但我們并不知道在函數中對傳入的auto_ptr做了什么,如果當中某些操作使其失去了對對象的所有權,那么這還是可能會導致致命的執行期錯誤。
結論:const reference是智能指針作為參數傳遞的底線。
7)auto_ptr不能初始化為指向非動態內存
? ? ? ?原因很簡單,delete 表達式會被應用在不是動態分配的指針上這將導致未定義的程序行為。
8)auto_ptr常用的成員函數
a、get()
? ? ? ?返回auto_ptr指向的那個對象的內存地址。
b、 reset()
? ? ? ?重新設置auto_ptr指向的對象。類似于賦值操作,但賦值操作不允許將一個普通指針直接賦給auto_ptr,而reset()允許。
注:reset(0)可以釋放對象,銷毀內存。
c、release()
? ? ? ?返回auto_ptr指向的那個對象的內存地址,并釋放對這個對象的所有權。
? ? ? ?用此函數初始化auto_ptr時可以避免兩個auto_ptr對象擁有同一個對象的情況(與get函數相比)。
?
6、scoped_ptr
? ? ? ?scoped_ptr是一個類似于auto_ptr的智能指針,它包裝了new操作符在堆上分配的動態對象,能夠保證動態創建的對象在任何時候都可以被正確的刪除。但是scoped_ptr的所有權更加嚴格,不能轉讓,一旦scoped_pstr獲取了對象的管理權,你就無法再從它那里取回來。正如scoped_ptr(局部指針)名字的含義:這個智能指針只能在作用域里使用,不希望被轉讓。
? ? ? ?scoped和weak_ptr的區別就是,給出了拷貝和賦值操作的聲明并沒有給出具體實現,并且將這兩個操作定義成私有的,這樣就保證scoped_ptr不能使用拷貝來構造新的對象也不能執行賦值操作,更加安全,但有了”++”“–”以及“*”“->”這些操作,比weak_ptr能實現更多功能。
(1)scoped_ptr用法
? ? ? ?scoped_ptr的用法與普通的指針幾乎沒什么區別;最大的差別在于你不必再記得在指針上調用delete,還有就是scoped_ptr不允許復制。典型的指針操作(operator* 和 operator->)都被重載了,并提供了和裸指針一樣的語法。用scoped_ptr和用裸指針一樣快,也沒有大小上的增加,因此它們可以廣泛使用。
(2)成員函數
1)explicit scoped_ptr(T* p=0)?
? ? ? ?構造函數,存儲p的一份拷貝。注意,p 必須是用operator new分配的,或者是null. 在構造的時候,不要求T必須是一個完整的類型。當指針p是調用某個分配函數的結果而不是直接調用new得到的時候很有用:因為這個類型不必是完整的,只需要類型T的一個前向聲明就可以了。這個構造函數不會拋出異常。
2)~scoped_ptr()?
? ? ? ?刪除指針所指向的對象。類型T在被銷毀時必須是一個完整的類型。如果scoped_ptr在它被析構時并沒有保存資源,它就什么都不做。這個析構函數不會拋出異常。
3)void reset(T* p=0);?
? ? ? ?重置一個 scoped_ptr 就是刪除它已保存的指針,如果它有的話,并重新保存p. 通常,資源的生存期管理應該完全由scoped_ptr自己處理,但是在極少數時候,資源需要在scoped_ptr的析構之前釋放,或者scoped_ptr要處理它原有資源之外的另外一個資源。這時,就可以用reset,但一定要盡量少用它。(過多地使用它通常表示有設計方面的問題) 這個函數不會拋出異常。
4)T& operator*() const;?
? ? ? ?該運算符返回一個智能指針中存儲的指針所指向的對象的引用。由于不允許空的引用,所以解引用一個擁有空指針的scoped_ptr將導致未定義行為。如果不能肯定所含指針是否有效,就用函數get替代解引用。這個函數不會拋出異常。
5)T* operator->() const;?
? ? ? ?返回智能指針所保存的指針。如果保存的指針為空,則調用這個函數會導致未定義行為。如果不能肯定指針是否空的,最好使用函數get。這個函數不會拋出異常。
6)T* get() const;?
? ? ? ?返回保存的指針。應該小心地使用get,因為它可以直接操作裸指針。但是,get使得你可以測試保存的指針是否為空。這個函數不會拋出異常。get通常在調用那些需要裸指針的函數時使用。
7)operator unspecified_bool_type() const?
? ? ? ?返回scoped_ptr是否為非空。返回值的類型是未指明的,但這個類型可被用于Boolean的上下文(boolean context)中。在if語句中最好使用這個類型轉換函數,而不要用get去測試scoped_ptr的有效性。
8)void swap(scoped_ptr& b)?
? ? ? ?交換兩個scoped_ptr的內容。這個函數不會拋出異常。
參考:https://blog.csdn.net/flowing_wind/article/details/81301001
總結
以上是生活随笔為你收集整理的C++中智能指针详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 维克设备管理软件 v2.17 通用网络版
- 下一篇: TJA1050比pC8C250一个值得关