簡而言之,當內存分配在棧上時,不需要直接管理,而當內存分配在堆上時則需要手動回收,或者等到堆上內存分配滿了觸發了自動回收機制。
一個由C/C++編譯的程序占用的內存分為以下幾個部分
這是一個前輩寫的,非常詳細
//main.cpp
int a = 0; 全局初始化區
char *p1; 全局未初始化區
main() {
int b; 棧
char s[] = "abc"; 棧
char *p2; 棧
char *p3 = "123456"; 123456/0在常量區,p3在棧上。
static int c =0; 全局(靜態)初始化區
p1 = (char *)malloc(10);
p2 = (char *)malloc(20);
分配得來得10和20字節的區域就在堆區。
strcpy(p1, "123456"); 123456/0放在常量區,編譯器可能會將它與p3所指向的"123456" 優化成一個地方。
}
注意,除了上文的malloc,new分配的內存也在堆中需要手動銷毀。
由上文看出,分配在堆上的內存需要手動進行動態分配和釋放,我們將之稱為動態內存。C++中,動態內存是通過new和delete來進行分配和釋放的。
new:在動態內存中為對象分配空間並返回一個指向該對象的指針,我們可以選擇對對象進行初始化。
delete:接受一個動態對象的指針,銷毀該對象,並釋放與之關聯的內存。
在自由空間分配的內存是無名的,因此new無法為其分配的對象命名,而是返回一個指向該對象的指針。
int *pi=new int; //pi指向一個動態分配的,未初始化的無名對象
可以是使用直接初始化方式來初始化一個動態分配的對象。
int *pi=new int(1024);
string *ps=new string(10,’9’);
也可以對動態分配的對象進行值初始化,只需在類型名之後跟一對空括號即可:
string *ps1=new string; //默認初始化為空string
string *ps=new string(); //值初始化為空string
動態分配const對象
類似於其他任何const對象,一個動態分配的const對象必須進行初始化。對於一個定義了默認構造函數的類類型,其const動態對象可以隱式初始化,而其他類型的對象就必須顯式初始化。
一旦一個程序用光了它所有可用的內存,new表達式就會失敗。默認情況下,如果new不能分配所要求的內存空間,他會拋出一個類型為bad_alloc的異常。我們可以改變使用new的方式來阻止他拋出異常:
int *p1=new int; //如果分配失敗,new拋出std::bad_alloc
int *p2=new (nothrow) int //如果分配失敗,new返回一個空指針
delete p; //p必須指向一個動態分配的對象或是一個空指針
釋放一塊並非new分配的內存,或者將相同的指針值釋放多次,其行為是未定義的。
int i,*pil=&i,*pi2=nullptr;
double *pd=new double(33),*pd2=pd;
delete i; //錯誤:i不是一個指針
delete pil; //未定義:pil指向一個局部變量
delete pd; //正確
delete pd2; //未定義:pd2指向的內存已經被釋放了
delete pi2; //正確:釋放一個空指針總是沒有錯誤的
對於通過內置指針類型來管理的動態對象,直到被顯式釋放之前他都是存在的。
在delete之後,指針就變成了空懸指針,即,指向一塊曾經保存數據對象但現在已經無效的內存的指針。
如果我們需要保留指針,可以在delete之後將nullptr賦予指針,這樣就清除地指出指針不指向任何對象。
智能指針與常規指針的重要區別是它負責自動釋放所指向的對象,兩種智能指針的區別在於管理底層指針的方式:
shared_ptr允許多個指針指向同一個對象;unique_ptr則“獨占”所指向的對象。標准庫還定義了一個名為week_ptr的伴隨類,它是一種弱引用,指向shared_ptr所管理的對象。這三種類型都定義在memory頭文件中。
shared_ptr sp
, unique_ptr up
空智能指針,可以指向類型為T的對象
p
將p用作一個條件判斷,若p指向一個對象,則為true
*p
解引用p,獲得它指向的對象
p->mem
等價於(*p).mem
p.get()
返回p中保存的指針。要小心使用,若智能指針釋放了其對象,返回的指針所指向的對象也就消失了
swap(p,q) ,p.swap(q)
交換p和q中的指針
創建一個智能指針時,必須提供額外的信息——指針可以指向的類型。
shared_ptr p1;
shared_ptr> p2;
make_shared(args)
返回一個shared_ptr,指向一個動態分配的類型為T的對象。使用args初始化此對象
shared_ptrp(q)
p是shared_ptr q的拷貝;此操作會遞增q中的計數器。q中的指針必須能轉換為T*
p=q
p和q都是shared_ptr,所保存的指針必須能相互轉換。此操作會遞減p的引用計數,遞增q的引用計數;若p的引用計數變為0,則將其管理的原內存釋放
p.unique()
若p.use_count()為1,返回true;否則返回false
p.use_count()
返回與p共享對象的智能指針數量;可能很慢,主要用於調試
最安全的分配和使用動態內存的方法是調用一個名為make_shared的標准庫函數。此函數在動態內存中分配一個對象並初始化它,返回指向此對象的shared_ptr。
shared_ptr p3=make_shared(42);
auto p6=make_shared>();
shared_ptr的拷貝和賦值
當進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的對象。
shared_ptr自動銷毀所管理的對象
shared_ptr的析構函數會遞減它所指向的對象的引用計數。如果引用計數變為0,shared_ptr的析構函數就會銷毀對象,並釋放它占用的內存。
shared_ptr還會自動釋放相關聯的內存
return會對shared_ptr指針的引用次數進行遞增操作。
使用了動態生存期的資源的類
程序使用動態內存出於以下三種原因之一:
1. 程序不知道自己需要使用多少對象
2. 程序不知道所需對象的准確類型
3. 程序需要在多個對象間共享數據
接受指針參數的智能指針構造函數是explicit的,我們不能將一個內置指針隱式轉換為一個智能指針,必須使用直接初始化形式來初始化一個智能指針:
shared_ptr p1=new int(1024); //錯誤
shared_ptr p2(new int(1024)); //正確
shared_ptr clone(int p){
return new int(p); //錯誤
}
shared_ptr clone(int p){
return shared_ptr(new int(p)); //正確
}
定義和改變shared_ptr的其他方法
shared_ptr p(q)
p管理內置指針q所指向的對象;q必須指向new分配的內存,且能夠轉換為T*類型
shared_ptr p(u)
p從unique_ptr u那裡接管了對象的所有權;將u置為空
shared_ptr p(q,d)
p接管了內置指針q所指向的對象的所有權。q必須能轉換為T*類型。p將使用可調用對象d來代替delete
shared_ptr p(p2,d)
p是shared_ptr p2的拷貝,唯一的區別是p將用可調用對象d來代替delete
p.reset()p,reset(q)p,reset(q,d)
若p是唯一指向其對象的shared_ptr,reset會釋放此對象。若傳遞了可選的參數內置指針q,會令p指向q,否則將p置為空。若還傳遞了參數d,將會調用d而不是delete來釋放q
不要混合使用普通指針和智能指針
void process(shared_ptr ptr){}
process的參數是傳值方式傳遞的,因此實參會被拷貝到ptr中。拷貝一個shared_ptr會遞增其引用計數,因此,在process運行過程中,引用計數值至少為2.當process結束時,ptr的引用計數會遞減,但不會變為0.因此當局部變量ptr被銷毀時,ptr指向的內存不會被釋放。
正確方式是傳遞給它一個shared_ptr:
shared_ptr p(new int(42)); //引用計數為1
process(p); //
雖然不能傳遞給process一個內置指針,但可以傳遞給它一個(臨時的)shared_ptr,這個shared_ptr是用一個內置指針顯式構造的。但是,這樣做很可能會導致錯誤:
int *x(new int(1024)); //危險:x是一個普通指針,不是一個智能指針
process(x); //錯誤
process(shared_ptr(x)); //合法的,但內存會被釋放
int j=*x; //未定義的:x是一個空懸指針
不要使用get初始化另一個智能指針或為智能指針賦值
智能指針類型頂一個了一個名為get的函數,它返回一個內置指針,指向智能指針管理的對象。此函數是為了這樣一種情況兒設計的:我們需要向不能使用智能指針的代碼傳遞一個內置指針。使用get返回的指針的代碼不能delete此指針。
shared_ptr p(new int(42)); //引用計數為1
int *q=p.get(); //正確:但使用q時要注意,不要讓它管理的指針被釋放
{
//未定義:兩個獨立的shared_ptr指向相同的內存
shared_ptr(q);
}//程序塊結束,q被銷毀,它指向的內存被釋放
int foo=*p; //未定義:p指向的內存已經被釋放了
如果使用智能指針,即使程序塊過早結束,智能指針類也能確保在內存不再需要時將其釋放。
智能指針和啞類
使用我們自己的釋放操作
為了正確使用智能指針,我們必須堅持一些基本規范:
一個unique_ptr“擁有”它所指向的對象。與shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定對象。當unique_ptr被銷毀時,它所指向的對象也被銷毀。
與shared_ptr不同,沒有類似make_shared的標准庫函數返回一個unique_ptr。當我們定義一個unique_ptr時,需要將其綁定到一個new返回的指針上。尅死shared_ptr,初始化unique_ptr必須采用直接初始化形式:
unique_ptr p1; //可以指向一個double的unique_ptr
unique_ptr p2(new int(42));//p2指向一個值為42的int
由於一個unique_ptr擁有它指向的對象,因此unique_ptr不支持普通的拷貝或賦值操作
unique_ptr操作
unique_ptr u1
,unique_ptr u2
空unique_ptr,可以指向類型為T的對象,u1會使用delete來釋放它的指針;u2會使用一個類型為b的可調用對象來釋放它的指針
unique_ptr u(d)
空unique_ptr,指向類型為T的對象,用類型為D的對象d代替delete
u=nullptr
釋放u指向的對象,將u置為空
u.release()
u放棄對指針的控制權,返回指針,並將u置為空
u.reset()
釋放u指向的對象
u.reset(q) ,u.reset(nullptr)
如果提供了內置指針q,令u指向這個對象;否則將u置為空
//將所有權從p1轉移給p2
unique_ptr p2(p1.release()); //release將p1置為空
unique_ptr p3(new string(“Trex”));
//將所有權從p3轉移給p2
p2.reset(p3.release());; //reset釋放了p2原來指向的內存
傳遞unique_ptr參數和返回unique_ptr
不能拷貝unique_ptr的規則有一個例外:我們可以拷貝或賦值一個將要被銷毀的unique_ptr。最常見的例子是從函數返回一個unique_ptr。
向unique_ptr傳遞刪除器
weak_ptr指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。
weak_ptr
weak_ptr w
空weak_ptr可以指向類型為T的對象
weak_ptr w(sp)
與shared_ptr sp指向相同對象的weak_ptr。T必須能轉換為sp指向的類型
w=p
p可以是一個shared_ptr或一個weak_ptr。賦值後w與p共享對象
w.reset()
將w置為空
w.use_count()
與w共享對象的shared_ptr的數量
w.expired()
若w.use_count()為0,返回true,否則返回false
w.lock()
如果expired為true,返回一個空shared_ptr;否則返回一個指向w的對象的shared_ptr
new和delete運算符一次分配/釋放一個對象,但某些應用需要一次為很多對象分配內存的功能。例如,vector和string都是在連續內存中保存它們的元素,因此,當容器需要重新分配內存時,必須一次性為很多元素分配內存。
為了支持這種需求,C++語言和標准庫提供了兩種一次分配一個對象數組的方法:C++語言定義了動態數組的new方式;標准庫中包含了一個名為allocator的類。
int *pia=new int[get_size()]; //pia指向第一個int
分配一個數組會得到一個元素類型的指針
雖然我們通常稱new T[]分配的內存為“動態數組”,但我們用new分配一個數組時,並未得到一個數組類型的對象,而是一個數組元素類型的指針。
由於分配的內存並不是一個數組類型,因此不能對動態數組調用begin或end。這些函數使用數組維度來返回指向首元素和尾後元素的指針。出於相同的原因,也不能用范圍for語句來處理動態數組中的元素。
初始化動態分配對象的數組
可以對數組中的元素進行值初始化,方法是在大笑之後跟一對空括號:
int *pia=new int[10]; //10個未初始化的int
int *pia2=new int[10](); //10個值初始化為0的int
動態分配一個空數組是合法的
雖然我們不能創建一個大小為0的靜態數組對象,但當n等於0時,調用new[n]是合法的:
char arr[0]; //錯誤
char *cp=new char[0]; //正確
釋放動態數組
為了釋放動態數組,我們使用一種特殊形式的delete——在指針前加上一個空方括號對:
delete p; //p必須指向一個動態分配的對象或為空
delete [] pa; //pa必須指向一個動態分配的數組或為空
數組中的元素按逆序被銷毀。
智能指針和動態數組
為了用一個unique_ptr管理動態數組,我們必須在對象類型後面跟一對空方括號:
//up指向一個包含10個未初始化int的數組
unique_ptr up(new int[10]);
up.release(); //自動用delete[]銷毀其指針
指向數組的unique_ptr
指向數組的unique_ptr不支持成員訪問運算符(點和箭頭運算符)
其他unique_ptr操作不便
unique_ptr u
u可以指向一個動態分配的數組,數組元素類型為T
unique_ptr u(p)
u指向內置指針p所指向的動態分配的數組。p必須能轉換為類型T*
u[i]
返回u擁有的數組中的位置i處的對象,u必須指向一個數組
標准庫allocator類定義在頭文件memory中,它幫助我們將內存分配和對象構造分離開來。它提供一種類型感知的內存分配方法,它分配的內存是原始的、未構造的。
類似vector,allocator是一個模板。為了定義一個allocator對象,我們必須指明這個allocator可以分配的對象類型。當一個allocator對象分配內存時,他會根據給定的對象類型來確定恰當的內存大小和對齊位置:
allocator alloc; //可以分配string的allocator對象
auto const p=alloc.allocate(n); //分配n個未初始化的string
標准庫allocator類及其算法
allocator分配為構造的內存
allocator分配的內存是未構造的,我們按需要在此內存中構造對象。
auto q=p; //q指向最後構造的元素之後的位置
alloc.construct(q++); //*q為空字符串
alloc.construct(q++,10,’c’); //*q為cccccccccc
alloc.construct(q++,”hi”); //*q位hi!
為了使用allocate返回的內存,我們必須用construct構造對象。使用為構造的內存,其行為是未定義的。
我們只能對真正構造了的元素進行destroy操作
拷貝和填充未初始化內存的算法
它們都定義在頭文件memory中
這些函數在給定目的位置創建元素,而不是由系統分配內存給它們。