C++ - [動態內存與智能指針]

參考資料:《C++ Primer中文版 第五版》

1、簡介

我們知道除了靜態內存和棧內存外,每個程序還有一個內存池,這部分內存被稱爲自由空間或者堆。程序用堆來存儲動態分配的對象即那些在程序運行時分配的對象,當動態對象不再使用時,我們的代碼必須顯式的銷燬它們。

在C++中,動態內存的管理是用一對運算符完成的:new和delete,new:在動態內存中爲對象分配一塊空間並返回一個指向該對象的指針,delete:指向一個動態獨享的指針,銷燬對象,並釋放與之關聯的內存。

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

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

2、shared_ptr類

1、創建智能指針時必須提供額外的信息,指針可以指向的類型:

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

2、默認初始化的智能指針中保存着一個空指針。
智能指針的使用方式和普通指針類似,解引用一個智能指針返回它指向的對象,在一個條件判斷中使用智能指針就是檢測它是不是空。

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

如下表所示是shared_ptr和unique_ptr都支持的操作:
在這裏插入圖片描述
如下表所示是shared_ptr特有的操作:
在這裏插入圖片描述

3、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用其參數來構造給定類型的對象,如果我們不傳遞任何參數,對象就會進行值初始化

4、shared_ptr的拷貝和賦值
當進行拷貝和賦值時,每個shared_ptr都會記錄1有多少個其他shared_ptr都會記錄有多少個其他shared_ptr指向相同的對象。

auto p = make_shared(42);
auto q§;
1
2
我們可以認爲每個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原來指向的對象已沒有引用者,會自動釋放

5、shared_ptr自動銷燬所管理的對象
當指向一個對象的最後一個shared_ptr被銷燬時,shared_ptr類會自動銷燬此對象,它是通過另一個特殊的成員函數-析構函數完成銷燬工作的,類似於構造函數,每個類都有一個析構函數。析構函數控制對象銷燬時做什麼操作。析構函數一般用來釋放對象所分配的資源。shared_ptr的析構函數會遞減它所指向的對象的引用計數。如果引用計數變爲0,shared_ptr的析構函數就會銷燬對象,並釋放它所佔用的內存。

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

使用了動態生存期的資源的類:
程序使用動態內存的原因:
(1)程序不知道自己需要使用多少對象
(2)程序不知道所需對象的準確類型
(3)程序需要在多個對象間共享數據

6、直接管理內存
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賦予指針,這樣就清楚的指出指針不指向任何對象。
動態內存的一個基本問題是可能多個指針指向相同的內存

7、shared_ptr和new結合使用
如果我們不初始化一個智能指針,它就會被初始化成一個空指針,接受指針參數的智能指針是explicit的,因此我們不能將一個內置指針隱式轉換爲一個智能指針,必須直接初始化形式來初始化一個智能指針

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

下表爲定義和改變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類型的對象時,必須提供一個指定類型的可調用對象刪除器。

3、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將返回一個空指針

4、scoped_ptr

scoped和weak_ptr的區別就是,給出了拷貝和賦值操作的聲明並沒有給出具體實現,並且將這兩個操作定義成私有的,這樣就保證scoped_ptr不能使用拷貝來構造新的對象也不能執行賦值操作,更加安全,但有了”++”“–”以及“*”“->”這些操作,比weak_ptr能實現更多功能。
————————————————
版權聲明:本文爲CSDN博主「Billy12138」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/flowing_wind/article/details/81301001

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章