1. 程式人生 > 實用技巧 >【C++】智慧指標詳解

【C++】智慧指標詳解

轉自:https://blog.csdn.net/flowing_wind/article/details/81301001

參考資料:《C++ Primer中文版 第五版》
我們知道除了靜態記憶體和棧記憶體外,每個程式還有一個記憶體池,這部分記憶體被稱為自由空間或者堆。程式用堆來儲存動態分配的物件即那些在程式執行時分配的物件,當動態物件不再使用時,我們的程式碼必須顯式的銷燬它們。

在C++中,動態記憶體的管理是用一對運算子完成的:new和delete,new:在動態記憶體中為物件分配一塊空間並返回一個指向該物件的指標,delete:指向一個動態獨享的指標,銷燬物件,並釋放與之關聯的記憶體。

動態記憶體管理經常會出現兩種問題:一種是忘記釋放記憶體,會造成記憶體洩漏;一種是尚有指標引用記憶體的情況下就釋放了它,就會產生引用非法記憶體的指標。

為了更加容易(更加安全)的使用動態記憶體,引入了智慧指標的概念。智慧指標的行為類似常規指標,重要的區別是它負責自動釋放所指向的物件。標準庫提供的兩種智慧指標的區別在於管理底層指標的方法不同,shared_ptr允許多個指標指向同一個物件,unique_ptr則“獨佔”所指向的物件。標準庫還定義了一種名為weak_ptr的伴隨類,它是一種弱引用,指向shared_ptr所管理的物件,這三種智慧指標都定義在memory標頭檔案中。

#shared_ptr類
建立智慧指標時必須提供額外的資訊,指標可以指向的型別:

shared_ptr<string> p1;
shared_ptr<list<int>> p2;

預設初始化的智慧指標中儲存著一個空指標。
智慧指標的使用方式和普通指標類似,解引用一個智慧指標返回它指向的物件,在一個條件判斷中使用智慧指標就是檢測它是不是空。

if(p1  && p1->empty())
	*p1 = "hi";

如下表所示是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用其引數來構造給定型別的物件,如果我們不傳遞任何引數,物件就會進行值初始化

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的解構函式會遞減它所指向的物件的引用計數。如果引用計數變為0,shared_ptr的解構函式就會銷燬物件,並釋放它所佔用的記憶體。

shared_ptr還會自動釋放相關聯的記憶體
當動態物件不再被使用時,shared_ptr類還會自動釋放動態物件,這一特性使得動態記憶體的使用變得非常容易。如果你將shared_ptr存放於一個容器中,而後不再需要全部元素,而只使用其中一部分,要記得用erase刪除不再需要的那些元素。

使用了動態生存期的資源的類:
程式使用動態記憶體的原因:
(1)程式不知道自己需要使用多少物件
(2)程式不知道所需物件的準確型別
(3)程式需要在多個物件間共享資料

直接管理記憶體
C++定義了兩個運算子來分配和釋放動態記憶體,new和delete,使用這兩個運算子非常容易出錯。

使用new動態分配和初始化物件
在自由空間分配的記憶體是無名的,因此new無法為其分配的物件命名,而是返回一個指向該物件的指標。

int *pi = new int;//pi指向一個動態分配的、未初始化的無名物件

此new表示式在自由空間構造一個int型物件,並返回指向該物件的指標

預設情況下,動態分配的物件是預設初始化的,這意味著內建型別或組合型別的物件的值將是未定義的,而類型別物件將用預設建構函式進行初始化。

string *ps = new string;//初始化為空string
int *pi = new int;//pi指向一個未初始化的int

我們可以直接使用直接初始化方式來初始化一個動態分配一個動態分配的物件。我們可以使用傳統的構造方式,在新標準下,也可以使用列表初始化

int *pi = new int(1024);
string *ps = new string(10,'9');
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

也可以對動態分配的物件進行初始化,只需在型別名之後跟一對空括號即可;

動態分配的const物件

const int *pci = new const int(1024);
//分配並初始化一個const int
const string *pcs = new const string;
//分配並預設初始化一個const的空string

類似其他任何const物件,一個動態分配的const物件必須進行初始化。對於一個定義了預設建構函式的類型別,其const動態物件可以隱式初始化,而其他型別的物件就必須顯式初始化。由於分配的物件就必須顯式初始化。由於分配的物件是const的,new返回的指標就是一個指向const的指標。

記憶體耗盡:
雖然現代計算機通常都配備大容量內村,但是自由空間被耗盡的情況還是有可能發生。一旦一個程式用光了它所有可用的空間,new表示式就會失敗。預設情況下,如果new不能分配所需的記憶體空間,他會丟擲一個bad_alloc的異常,我們可以改變使用new的方式來阻止它丟擲異常

//如果分配失敗,new返回一個空指標
int *p1 = new int;//如果分配失敗,new丟擲std::bad_alloc
int *p2 = new (nothrow)int;//如果分配失敗,new返回一個空指標

我們稱這種形式的new為定位new,定位new表示式允許我們向new傳遞額外的引數,在例子中我們傳給它一個由標準庫定義的nothrow的物件,如果將nothrow傳遞給new,我們的意圖是告訴它不要丟擲異常。如果這種形式的new不能分配所需記憶體,它會返回一個空指標。bad_alloc和nothrow都在標頭檔案new中。

釋放動態記憶體
為了防止記憶體耗盡,在動態記憶體使用完之後,必須將其歸還給系統,使用delete歸還。

指標值和delete
我們傳遞給delete的指標必須指向動態記憶體,或者是一個空指標。釋放一塊並非new分配的記憶體或者將相同的指標釋放多次,其行為是未定義的。即使delete後面跟的是指向靜態分配的物件或者已經釋放的空間,編譯還是能夠通過,實際上是錯誤的。

動態物件的生存週期直到被釋放時為止
由shared_ptr管理的記憶體在最後一個shared_ptr銷燬時會被自動釋放,但是通過內建指標型別來管理的記憶體就不是這樣了,內建型別指標管理的動態物件,直到被顯式釋放之前都是存在的,所以呼叫這必須記得釋放記憶體。

使用new和delete管理動態記憶體常出現的問題:
(1)忘記delete記憶體
(2)使用已經釋放的物件
(3)同一塊記憶體釋放兩次

delete之後重置指標值
在delete之後,指標就變成了空懸指標,即指向一塊曾經儲存資料物件但現在已經無效的記憶體的地址

有一種方法可以避免懸空指標的問題:在指標即將要離開其作用於之前釋放掉它所關聯的記憶體
如果我們需要保留指標可以在delete之後將nullptr賦予指標,這樣就清楚的指出指標不指向任何物件。
動態記憶體的一個基本問題是可能多個指標指向相同的記憶體

shared_ptr和new結合使用
如果我們不初始化一個智慧指標,它就會被初始化成一個空指標,接受指標引數的職能指標是explicit的,因此我們不能將一個內建指標隱式轉換為一個智慧指標,必須直接初始化形式來初始化一個智慧指標

shared_ptr<int> p1 = new int(1024);//錯誤:必須使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正確:使用了直接初始化形式
  • 1
  • 2

下表為定義和改變shared_ptr的其他方法:

不要混合使用普通指標和智慧指標
如果混合使用的話,智慧指標自動釋放之後,普通指標有時就會變成懸空指標,當將一個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

其他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;//現在我們知道自己是唯一的使用者,可以改變物件的值

智慧指標和異常
如果使用智慧指標,即使程式塊過早結束,智慧指標也能確保在記憶體不再需要時將其釋放,sp是一個shared_ptr,因此sp銷燬時會檢測引用計數,當發生異常時,我們直接管理的記憶體是不會自動釋放的。如果使用內建指標管理記憶體,且在new之後在對應的delete之前發生了異常,則記憶體不會被釋放。

使用我們自己的釋放操作
預設情況下,shared_ptr假定他們指向的是動態記憶體,因此當一個shared_ptr被銷燬時,會自動執行delete操作,為了用shared_ptr來管理一個connection,我們必須首先必須定義一個函式來代替delete。這個刪除器函式必須能夠完成對shared_ptr中儲存的指標進行釋放的操作。

智慧指標陷阱:
(1)不使用相同的內建指標值初始化(或reset)多個智慧指標。
(2)不delete get()返回的指標
(3)不使用get()初始化或reset另一個智慧指標
(4)如果你使用get()返回的指標,記住當最後一個對應的智慧指標銷燬後,你的指標就變為無效了
(5)如果你使用智慧指標管理的資源不是new分配的記憶體,記住傳遞給它一個刪除器
#unique_ptr
某個時刻只能有一個unique_ptr指向一個給定物件,由於一個unique_ptr擁有它指向的物件,因此unique_ptr不支援普通的拷貝或賦值操作。
下表是unique的操作:

雖然我們不能拷貝或者賦值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<int> clone(int p)
{
	//正確:從int*建立一個unique_ptr<int>
	return unique_ptr<int>(new int(p));
}

還可以返回一個區域性物件的拷貝:

unique_ptr<int> clone(int p)
{
	unique_ptr<int> ret(new int(p));
	return ret;
}

向後相容:auto_ptr
標準庫的較早版本包含了一個名為auto_ptr的類,它具有uniqued_ptr的部分特性,但不是全部。
用unique_ptr傳遞刪除器
unique_ptr預設使用delete釋放它指向的物件,我們可以過載一個unique_ptr中預設的刪除器
我們必須在尖括號中unique_ptr指向型別之後提供刪除器型別。在建立或reset一個這種unique_ptr型別的物件時,必須提供一個指定型別的可呼叫物件刪除器。

#weak_ptr
weak_ptr是一種不控制所指向物件生存期的智慧指標,它指向一個由shared_ptr管理的物件,將一個weak_ptr繫結到一個shared_ptr不會改變shared_ptr的引用計數。一旦最後一個指向物件的shared_ptr被銷燬,物件就會被釋放,即使有weak_ptr指向物件,物件還是會被釋放。
weak_ptr的操作

由於物件可能不存在,我們不能使用weak_ptr直接訪問物件,而必須呼叫lock,此函式檢查weak_ptr指向的物件是否存在。如果存在,lock返回一個指向共享物件的shared_ptr,如果不存在,lock將返回一個空指標

#scoped_ptr
scoped和weak_ptr的區別就是,給出了拷貝和賦值操作的宣告並沒有給出具體實現,並且將這兩個操作定義成私有的,這樣就保證scoped_ptr不能使用拷貝來構造新的物件也不能執行賦值操作,更加安全,但有了"++""–"以及“*”“->”這些操作,比weak_ptr能實現更多功能。