內(nèi)存管理
C++使用new和delete兩個(gè)運(yùn)算符進(jìn)行內(nèi)存管理。
使用new進(jìn)行動(dòng)態(tài)分配和初始化對(duì)象
在自由空間內(nèi)分配的內(nèi)存是無(wú)名的,因此new無(wú)法為其分配的對(duì)象進(jìn)行命名,而是返回一個(gè)指向該對(duì)象的指針:
int *pi=new int;
上述表達(dá)式在自由空間(??臻g?堆空間?)內(nèi)構(gòu)造一個(gè) int對(duì)象,并且返回一個(gè)指向這個(gè)對(duì)象的指針。
默認(rèn)情況下,動(dòng)態(tài)分配的對(duì)象是默認(rèn)初始化的,這就意味著內(nèi)置類型(int)或者是組合類型的對(duì)象的值是未定義的,而類類型的對(duì)象將會(huì)使用默認(rèn)構(gòu)造函數(shù)進(jìn)行初始化:
string *ps=new string;
int *pi=new int;
舊標(biāo)準(zhǔn)下,可以使用直接初始化的方式進(jìn)行初始化一個(gè)動(dòng)態(tài)分配對(duì)象。也可以使用傳統(tǒng)的構(gòu)造方式(使用圓括號(hào))
int *pi=new int(1024); //使用直接初始化的方式初始化一個(gè)動(dòng)態(tài)分配對(duì)象
string *ps=new string(10,'9'); //使用傳統(tǒng)的構(gòu)造方式進(jìn)行初始化的過(guò)程
vector<int> *pv=new vector<int>{0,1,2,3,4,5,6}; //使用列表初始化的形式
也可以對(duì)動(dòng)態(tài)分配的對(duì)象進(jìn)行值初始化,只需要在類型名之后添加括號(hào)即可
string *ps1=new string; //默認(rèn)初始化為空 string
string *ps=new string(); //值初始化為空string
int *pi1 = new int; //默認(rèn)初始化,*pi1的值未定義
int *pi2 = new int(); //值初始化為0. *pi2 為 0 這種方式只能用于初始化指針,不能用于初始化int對(duì)象
值初始化內(nèi)置類型對(duì)象具有良好定義的值,默認(rèn)初始化對(duì)象的值則是未定義的。對(duì)于類中那些依賴編譯器射程的默認(rèn)構(gòu)造函數(shù)的內(nèi)置類型成員,沒(méi)有進(jìn)行類內(nèi)初始化的時(shí)候,他們的值也是未定義的。
如果具有括號(hào)包圍的初始化器,可以使用auto來(lái)推斷我們想要分配的對(duì)象的類型。(需要保證使用單一初始化器)
auto p1=new auto(obj); //p指向一個(gè)和obj類型相同的對(duì)象,該對(duì)象使用obj進(jìn)行初始化。
auto p2=new auto{a,b,c};//括號(hào)中只能有單個(gè)初始化器
動(dòng)態(tài)分配的const對(duì)象
使用new 分配const對(duì)象是合法的
//分配并初始化一個(gè)const int
const int *pci=new const int(1024);
//分配并初始化一個(gè)const的空string
const string *pcs = new const string();
類似其他的任何const對(duì)象,一個(gè)動(dòng)態(tài)分配的const的對(duì)象必須進(jìn)行初始化。對(duì)于一個(gè)定義了默認(rèn)構(gòu)造函數(shù)的類類型,其const動(dòng)態(tài)對(duì)象可以陰式初始化。由于分配的對(duì)象是const的,new返回的指針是指向一個(gè)const的指針。
內(nèi)存耗盡
當(dāng)一個(gè)程序用光了所有的可用內(nèi)存之后,new表達(dá)式將會(huì)失敗,默認(rèn)情況下,當(dāng)new不能分配所要求的內(nèi)存空間的時(shí)候,將會(huì)拋出一個(gè)類型為bad_alloc的異常,可以通過(guò)改變new的使用方式阻止其拋出異常:
int *pi=new int();//如果分配失敗,new將會(huì)拋出std::bad_alloc
int *p2=new (nothrow) int();//如果分配失敗,new返回一個(gè)空指針。
我們將這種形式的new稱為定位new。定位new 表達(dá)式允許我們想new傳遞額外的參數(shù)。在上述例子中,我們傳遞給它一個(gè)標(biāo)準(zhǔn)庫(kù)定義的名為nothrow的對(duì)象。
釋放動(dòng)態(tài)內(nèi)存
為了防止內(nèi)存耗盡,在動(dòng)態(tài)內(nèi)存使用完畢之后,必須要將其歸還給系統(tǒng),我們通過(guò)使用delete表達(dá)式,將動(dòng)態(tài)內(nèi)存歸還給系統(tǒng)。delete表達(dá)式接收一個(gè)指針,指向我們想要釋放的對(duì)象。
delete p;//p需要指向一個(gè)動(dòng)態(tài)分配對(duì)象或者是一個(gè)空指針
delete表達(dá)式執(zhí)行兩個(gè)操作:銷(xiāo)毀給定指針指向的對(duì)象;釋放對(duì)應(yīng)的內(nèi)存。
指針值和delete
傳遞給delete的指針必須指向動(dòng)態(tài)分配的內(nèi)存,或者是一個(gè)空指針。釋放一塊并非new分配的內(nèi)存,或者是將相同的指針釋放多次,其行為是未定義的:
int i,*pi1 = &i,*pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i;//錯(cuò)誤:i并非一個(gè)指針
delete pi1;//未定義:pi1指向一個(gè)局部變量
delete pd;//正確
delete pd2;//未定義: pd2指向的內(nèi)存已經(jīng)被釋放了
delete pi2;//正確: 釋放一個(gè)空指針總是沒(méi)有問(wèn)題的
對(duì)于delete pi1和pd2所產(chǎn)生的錯(cuò)誤是具有潛在危害的:通常情況下,編譯器不能分辨一個(gè)指針指向靜態(tài)還是動(dòng)態(tài)分配的對(duì)象。
雖然一個(gè)const對(duì)象的值不能被改變,但是是可以被銷(xiāo)毀的。
動(dòng)態(tài)對(duì)象的生存期直到被釋放為止
調(diào)用者使用返回指向動(dòng)態(tài)內(nèi)存的指針的函數(shù)的時(shí)候,需要進(jìn)行手動(dòng)的內(nèi)存釋放。
//factory 返回一個(gè)指針,指向一個(gè)動(dòng)態(tài)分配的對(duì)象
Foo* factory(T arg){
return new Foo(arg);//調(diào)用者負(fù)責(zé)釋放這個(gè)內(nèi)存
}
調(diào)用者需要避免使用了但是未釋放的情況,如下所示:
void use_factory(T arg){
Foo *p=factory(arg);//使用了p 但是沒(méi)有進(jìn)行delete
}//p離開(kāi)了它的作用域,但是它所指向的內(nèi)存并沒(méi)有被釋放。
內(nèi)置類型的對(duì)象被銷(xiāo)毀的時(shí)候什么也不會(huì)發(fā)生,特別是,當(dāng)一個(gè)指針離開(kāi)其作用域的時(shí)候,它所指向的對(duì)象什么也不會(huì)發(fā)生。如果這個(gè)指針指向的是動(dòng)態(tài)內(nèi)存,內(nèi)存將不會(huì)被自動(dòng)釋放。
使用new和delete進(jìn)行動(dòng)態(tài)內(nèi)存管理時(shí)常見(jiàn)的錯(cuò)誤
- 沒(méi)有使用delete進(jìn)行內(nèi)存釋放,導(dǎo)致發(fā)生內(nèi)存泄漏
- 使用已經(jīng)釋放掉的對(duì)象,通過(guò)釋放內(nèi)存之后將指針置為空
- 同一塊內(nèi)存釋放兩次。當(dāng)有兩個(gè)指針指向相同的動(dòng)態(tài)分配對(duì)象時(shí)候,有可能會(huì)對(duì)其中一個(gè)指針進(jìn)行了delete操作,隨即又對(duì)第二個(gè)指針進(jìn)行了delete操作,導(dǎo)致自由空間被破壞。
堅(jiān)持只使用只能指針,可以避免這些所有的問(wèn)題,對(duì)于一個(gè)內(nèi)存,只有在沒(méi)有任何智能指針指向他的時(shí)候,才會(huì)自動(dòng)進(jìn)行釋放。
delete之后重置指針值
當(dāng)我們delete一個(gè)指針之后,指針值就變成了無(wú)效。雖然指針已經(jīng)無(wú)效,但是指針仍然保存(已經(jīng)釋放了的)動(dòng)態(tài)內(nèi)存的地址。意味著這時(shí)候指針變成了空懸指針(指向一塊曾經(jīng)保存數(shù)據(jù)對(duì)象但是已經(jīng)無(wú)效的內(nèi)存的指針)。
未初始化指針的所有缺點(diǎn),空懸指針也有。為了解決這個(gè)問(wèn)題,可以在delete之后,將nullptr賦予指針,清楚的指向指針不指向任何對(duì)象。
動(dòng)態(tài)指針的問(wèn)題在于可能有多個(gè)指針指向了同一個(gè)內(nèi)存。delete內(nèi)存之后重置指針的方式只對(duì)當(dāng)前指針有效,對(duì)于仍然指向(已經(jīng)釋放掉)內(nèi)存的指針是沒(méi)有任何作用的。
同時(shí)在實(shí)際系統(tǒng)中,查找指向相同內(nèi)存的所有指針是異常困難的。
智能指針
c++標(biāo)準(zhǔn)庫(kù)提供了兩種智能指針來(lái)管理內(nèi)存對(duì)象。智能指針的功能類似于常規(guī)指針,但是只能指針可以自動(dòng)釋放所指向的對(duì)象。
shared_ptr允許多個(gè)指針指向同一個(gè)對(duì)象;unique_ptr則是獨(dú)占所指向的對(duì)象。
shared_ptr
只能指針是一種模板,當(dāng)創(chuàng)建了一個(gè)智能指針的時(shí)候,必須提供額外的信息-指針?biāo)赶虻念愋汀?/p>
shared_ptr<string> p1; //shared_ptr 可以指向string
shared_ptr<list<int>> p2; //shared_ptr 可以指向int的list
默認(rèn)初始化的智能指針中保存一個(gè)空指針。智能指針的使用方式和普通指針是類似的,通過(guò)解引用一個(gè)智能指針?lè)祷厮赶虻膶?duì)象。也可以用作判斷條件表明當(dāng)前智能指針是否為空。
//如果p1不為空,檢查它是否指向一個(gè)空string
if(p1 && p1->empty())
*p1 = "hi"; //如果p1指向一個(gè)空string ,解引用p1,將一個(gè)新值賦予string
shared_ptr和uniqu_ptr都具有的操作如下:
| shared_ptr<T> sp | 空智能指針,指向類型為T(mén)的對(duì)象 |
|---|---|
| unqiue_ptr<T> up | 空智能指針,指向類型為T(mén)的對(duì)象 |
| p | 將p作為一個(gè)條件判斷,如果p指向一個(gè)對(duì)象,則為true |
| *p | 解引用p,獲取其指向的對(duì)象 |
| p->mem | 等價(jià)于(*p).mem |
| p.get() | 返回p中保存的指針 |
| swap(p,q) | 交換p,q保存的指針 |
| p.swap(q) | 交換p,q保存的指針 |
shared_ptr所獨(dú)有的操作
| make_shared<T>(args) | 返回一個(gè)shared_ptr,指向一個(gè)動(dòng)態(tài)分配類型的T的對(duì)象,使用args初始化對(duì)象 |
|---|---|
| shared_ptr<T> p(q) | p是shared_ptr q的拷貝,這個(gè)操作會(huì)遞增q中的計(jì)數(shù)器,q中的指針必須可以轉(zhuǎn)化為T(mén)* |
| p=q | p和q都是shared_ptr,所保存的指針必須可以相互轉(zhuǎn)換。此操作會(huì)遞減p的引用計(jì)數(shù),遞增q的引用計(jì)數(shù);若p的引用計(jì)數(shù)變?yōu)?,將其管理的內(nèi)存進(jìn)行釋放 |
| p.unique() | 如果p.use_count()為1,返回true,否則返回false; |
| p.use_count() | 返回和p共享對(duì)象的智能指針的數(shù)量;主要用于調(diào)試 |
make_shared函數(shù)
最安全的分配和使用動(dòng)態(tài)內(nèi)存的方法是調(diào)用一個(gè)名為make_shard的標(biāo)準(zhǔn)庫(kù)函數(shù)。這個(gè)函數(shù)在動(dòng)態(tài)內(nèi)存中分配一個(gè)對(duì)象并進(jìn)行初始化。返回指向這個(gè)對(duì)象的shared_ptr。
使用make_shared函數(shù)的時(shí)候,必須制定想要?jiǎng)?chuàng)建的對(duì)象的類型。定義的方式和模板類相同。
//指向一個(gè)值為42的int的shared_ptr
shared_ptr<int> p3= make_shared<int>(42)
//p4指向一個(gè)值為“999999999”的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一個(gè)值初始化的int,即值為0
shared_ptr<int> p5 =make_shared<int>();
shared_ptr的拷貝和賦值
當(dāng)進(jìn)行拷貝和賦值操作的時(shí)候,每個(gè)shared_ptr都會(huì)記錄有多少個(gè)其他shared_ptr和其指向的是相同的對(duì)象:
我們可以認(rèn)為每個(gè)shared_ptr都是具有一個(gè)關(guān)聯(lián)計(jì)數(shù)器的,稱其為引用計(jì)數(shù)。無(wú)論何時(shí)我們拷貝一個(gè)shared_ptr,引用計(jì)數(shù)的值都會(huì)遞增。例如:
- 使用一個(gè)shared_ptr初始化另一個(gè)shared_ptr;
- 將shared_ptr作為一個(gè)參數(shù)傳遞給一個(gè)函數(shù);
- 作為函數(shù)的返回值;
當(dāng)我們給一個(gè)shared_ptr賦予一個(gè)新值或者是shared_ptr被銷(xiāo)毀 (一個(gè)局部的shared_ptr)離開(kāi)其作用域的時(shí)候,計(jì)數(shù)器就會(huì)進(jìn)行遞減。
一旦一個(gè)shared_ptr的計(jì)數(shù)器變?yōu)?,就會(huì)自動(dòng)釋放所管理的對(duì)象。
shared_ptr自動(dòng)銷(xiāo)毀所管理的對(duì)象
當(dāng)指向一個(gè)對(duì)象的最后一個(gè)shared_ptr被銷(xiāo)毀的時(shí)候,shared_ptr將會(huì)自動(dòng)銷(xiāo)毀這個(gè)對(duì)象。這是通過(guò)特殊的成員函數(shù)析構(gòu)函數(shù)完成銷(xiāo)毀的動(dòng)作。
shared_ptr自動(dòng)釋放相關(guān)聯(lián)的內(nèi)存
當(dāng)動(dòng)態(tài)對(duì)象不再被使用的時(shí)候,shared_ptr類將會(huì)自動(dòng)釋放動(dòng)態(tài)對(duì)象,這一特性使得動(dòng)態(tài)內(nèi)存的使用變得較為簡(jiǎn)單。
//factory返回一個(gè)shared_ptr,指向一個(gè)動(dòng)態(tài)分配的對(duì)象
shared_ptr<Foo> factory(T arg){
return make_shared<Foo>(arg);
}
由于factory返回一個(gè)shared_ptr,所以我們確保它分配的對(duì)象在恰當(dāng)?shù)臅r(shí)候被釋放。
由于在最后一個(gè)shared_ptr銷(xiāo)毀前內(nèi)存都不會(huì)釋放,保證shared_ptr在無(wú)用之后不再保留是非常重要的。
shared_ptr在無(wú)用之后的一保留的一種可能的情況是,將shared_ptr存放在了一個(gè)容器之中,隨后排了容器,從而不再需要某些元素,在這種情況下應(yīng)該確保使用erase刪除不再需要的shared_ptr元素
不要使用get初始化另一個(gè)智能指針或者是為智能指針賦值
智能指針類型定義了一個(gè)名為get的函數(shù),返回一個(gè)內(nèi)置指針,指向智能指針管理的對(duì)象。這種情況是為了這樣的一種情況設(shè)計(jì):我們需要向不能使用智能指針的代碼傳遞一個(gè)內(nèi)置指針。使用get返回指針的代碼不能delete這個(gè)指針。
使用了動(dòng)態(tài)生存期資源的類
程序使用動(dòng)態(tài)內(nèi)存主要出于以下的原因:
- 程序不知道自己需要使用多少對(duì)象
- 程序不知道所需對(duì)象的準(zhǔn)確類型
- 程序需要在多個(gè)對(duì)象之間共享數(shù)據(jù)。
智能指針和異常
如果使用智能指針,即使程序塊過(guò)早結(jié)束,智能指針類也能確保內(nèi)存在不再需要的時(shí)候?qū)⑵溽尫牛?/p>
void f(){
shared_ptr<int> sp(new int(42));//分配一個(gè)新對(duì)象
//這段代碼拋出一個(gè)異常,且在f中沒(méi)有被捕獲
}//在函數(shù)結(jié)束的時(shí)候,shared_ptr將會(huì)自動(dòng)釋放內(nèi)存
函數(shù)對(duì)象的退出有兩種可能,正常處理結(jié)束或者是發(fā)生了異常,無(wú)論哪種情況,局部對(duì)像都會(huì)被銷(xiāo)毀,而在局部對(duì)象銷(xiāo)毀的過(guò)程中將會(huì)檢查引用計(jì)數(shù),就會(huì)保證了智能指針在異常情況下,可以釋放掉內(nèi)存。