第二十章 動態內存
因翻譯太耗時,現做筆記如下:
12.1 動態內存和智能指針
new:在動態內存中爲對象分配空間並返回一個指向該對象的指針
delete:接受一個動態內存對象的指針,銷燬該對象,並釋放與之關聯的內存
使用動態內存容易出錯,主要是容易忘記釋放對象,和多次釋放。
因此,提供了兩種智能指針來管理動態內存。
shared_ptr:允許多個指針指向同一個對象
unique_ptr:一次只能一個指針指向對象
weak_ptr:一個弱引用,指向shared_ptr所管理的對象
12.1.1 shared_ptr
shared_ptr<string> p1;//shared_ptr,可以指向string
shared_ptr<list<int>> p2;//shared_ptr,可以指向int的list
默認初始化的智能指針保存着一個空指針。
智能指針的使用方法與普通的指針類似。解引用一個智能指針返回它指向的對象。如果在一個條件判斷中使用了智能指針,效果就是檢查他是否爲空
if(p1 && p1->empty())
*p1 = "hi";
下表列出了shared_ptr和unique_ptr都支持的操作
下表列出了shared_ptr才支持的操作
make_shared函數
該函數在動態內存中分配內存,並用給定的參數初始化它。
//p3指向一個值爲42的int
shared_ptr<int> p3 = make_shared<int>(42);
//p4指向一個值爲9999999999的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一個值初始化的int,值爲0
shared_ptr<int> p5 = make_shared<int>();
注意:make_shared接受的參數,必須和需要構造的類型的某一個構造函數相同。
shared_ptr的拷貝和賦值
當進行拷貝後者賦值操作的時候,每個shared_ptr都會記錄有多少個shared_ptr指向相同的對象。
auto p = make_shared<int>(42);
//p和q指向相同的對象,此對象有兩個引用者
auto q(p);
我們可以認爲,每一個shared_ptr對象都有一個關聯的計數器,稱之爲引用計數。無論何時拷貝一個shared_ptr,計數器都會增加。
當給shared_ptr賦予一個新值或是shared_ptr被銷燬時,計數器就會遞減。
一旦一個shared_ptr的計數器變爲0,他就會自動釋放自己所管理的對象。
auto r = make_shared<int>(42);
r=q;
//給r賦值,令他指向另外一個地址
//遞增q指向的對象
//遞減r原來指向的對象的引用計數
//當r原來指向的對象已沒有引用者,會自動釋放
shared_ptr自動銷燬所管理的對象
當引用計數變爲0時,shared_ptr釋放所指向的對象,在釋放之前,會調用對象的析構函數。
12.1.2 直接管理內存
使用new動態分配和初始化對象
在自由空間分配的內存是無名的,因此new無法爲其分配的對象命名,而是返回一個指向該對象的指針。
int *pi = new int;//pi指向一個動態分配的,未初始化的無名對象
默認情況下,動態內存分配的對象是默認初始化的。因此對於內置類型和組合類型的對象其值是未定義的。而類類型的對象,則使用默認構造函數進行初始化
string *ps = new string;//初始化爲空
int *pi = new int; //pi指向一個未初始化的int
還可以進行直接初始化和列表初始化如下:
int *pi = new int(1024);//pi指向的對象的值爲1024
string *ps = new string(10,'9');//*ps爲999999999
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};
還可以進行值初始化,如下:
string *ps1 = new string;//默認初始化
string *ps = new string() ;//值初始化
int *pi1 = new int;//默認初始化,*pi1的值未定義
int *pi2 = new int();//值初始化,爲0
注意上面的代碼,對於類類型來說,值初始化和默認初始化,都會調用他們的默認構造函數,最後的結果是相同的。而對於內置類型來說,默認初始化和值初始化就不一樣,默認初始化,其值是未定義的。
動態分配const對象
const int *pci = new const int(1024);
const string *pcs = new const string;
注意:如果new失敗,會拋出一個bad_alloc的異常。可以改變new的方式來阻止這種情況
int *p1 = new int;//如果分配失敗則,拋出std::bad_alloc
int *p2 = new (nothrow) int;//如果分配失敗,則new返回一個空指針
上例子中,這種new稱之爲,定位new。定位new允許我們向其傳遞額外的參數。
這種new如果分配失敗,則返回一個空指針。
釋放動態內存
delete p;//p必須指向一個動態內存分配的對象或是一個空指針
使用new和delete管理動態內存存在三個常見的問題:
- 忘記delete內存。
- 使用已經釋放的內存
- 同一塊內存釋放多次
delete之後重置指針
當我們delete一個指針之後,指針就變成無效的了。而此時指針的值還依然保存着
因此,如果不小心使用了這個指針,那麼就會使用一個已經釋放了的內存,所以
delete指針之後,需要將這個指針重置爲空。
12.1.3 shared_ptr和new結合使用
可以使用new返回的指針,來初始化shared_ptr
shared_ptr<double> p1;//shared_ptr可以指向一個double
shared_ptr<int> p2(new int(42));//p2指向一個值爲42的int
接受指針參數的智能指針的構造函數是explicit的,因此,注意下面的代碼
shared_ptr<int> p1 = new int(1024);//錯誤:必須使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正確:使用了直接初始胡形式
下面給出定義和改變shared_ptr的其他方法
不要混合使用智能指針和普通指針
考慮下面的例子:
void porcess(shared_ptr<int> ptr){
//使用ptr
}
int *x(new int(1024));//危險:x是一個普通指針,並不是一個智能指針
process(x);//錯誤:不能將int* 轉換爲一個shared_ptr<int>
process(shared_ptr<int>(x));//合法的,但是內存會被釋放
int j = *x;//未定義的:x是一個無效的指針
在上面例中,構造了一個臨時的shared_ptr傳遞給process函數,一旦函數運行
結束,那麼這個臨時的shared_ptr將被銷燬,它指向的對象將會釋放
此時,x指向的對象已經被釋放,x是一個無效的指針。
也不要使用get初始化另外一個智能指針或爲智能指針賦值
智能指針提供了一個get的成員,它返回智能指針指向的對象的地址。
這個成員函數是爲了如下情況進行設計的:需要向不使用智能指針的代碼
傳遞一個內置指針。
現在,思考下面的代碼
shared_ptr<int> p(new int(42));
int *q = p.get();//正確,但使用q的時候請注意,不要釋放它
{
shared_ptr<int> (q);
}//程序塊結束,q指向的對象,被釋放
int foo = *p;//未定義:p指向了一個已經被釋放的內存
其他的shared_ptr操作
reset
p = new int(1024);//錯誤:不能將一個指針賦值給一個shared_ptr
p.reset(new int(1024));//正確:p指向一個新對象
reset與賦值類似,會更新引用計數,如果需要會釋放p指向的對象。
12.1.4 智能指針和異常
void f(){
shared_ptr<int> sp(new int(42));
//其他代碼
}
函數退出可能兩種情況:1.正常結束;2.發生了異常。
無論哪種情況,局部對象都會被銷燬,因此,sp會被銷燬,它所指向的內存
也會被正確的釋放。
如果上面的程序,變成下面這樣:
void f(){
int *ip = new int(42);
//其他代碼
delete ip;
}
在上面例子中,如果正常結束,那麼ip會被正確釋放,但是如果遇到異常,ip將不會正確釋放
使用自定義的釋放操作
默認情況下,shared_ptr假定他們指向的都是動態內存。因此當一個shared_ptr
被銷燬的時,它默認對他管理的指針進行delete操作。
另外,我們還可以自定義自己的刪除操作,來代替默認的delete操作。
當創建一個shared_ptr時,可以傳遞一個表示刪除的函數,代替默認的delete操作。
void end_connection(connection *p){
disconnect(*p);
}
void f(destination &d /*其他參數*/){
connection c = connect(&d);
shared_ptr<connection> p(&c,end_connection);
//其他代碼
}
當p被銷燬的時候,不會直接調用delete函數,而是調用end_connection函數。
這樣,不管f函數是正常結束 還是異常退出,end_connection必定
會被調用。
上面函數的寫法可能看不出來有什麼好處,那麼作爲對比,請看下面的寫法
void f(destination &d,/*其他參數*/){
connection c = connect(&d);
//其他代碼
//最後,必須調用disconnect()進行資源的釋放
}
那麼上面的例子,一旦中間出現異常,disconnect將不會被調用
爲了能夠正確的使用智能指針,請堅持一些基本規範:
- 不使用相同的內置指針初始化多個智能指針
- 不delete get()返回的指針
- 不使用get()初始化或reset另外一個智能指針
- 如果你使用了get()返回的指針,記住當最後一個對應的智能指針被銷燬後,你的指針就會變爲無效了
- 如果你使用了智能指針管理的資源不是new分配的內存,記住傳遞給他一個刪除器
12.1.5 unique_ptr
與shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定的對象
當unique_ptr被銷燬的時候,他所指的對象也被銷燬
下表列出了unique_ptr特有的操作。與shared_ptr相同的操作在表12.1中
unique_ptr<double> p1;//可以指向一個double的unique_ptr
unique_ptr<int> p2(new int(42));//p2指向一個值爲42的int
由於unique_ptr擁有它指向的對象,因此unique_ptr不支持普通的拷貝或者賦值操作:
unique_ptr<string> p1(new string("Stegosaurus")):
unique_ptr<string> p2(p1);//錯誤,unique_ptr不支持拷貝
unique_ptr<string> p3;
p2 = p2;//錯誤:unique_ptr不支持賦值
雖然不能拷貝和賦值unique_ptr,但是可以通過release或者reset
將指針的所有權從一個轉移到另外一個。
unique_ptr<string> p2(p1.release());//release將p1置空
unique_Ptr<string> p3(new string("Trex"));
//將所有權從p3轉移到p2
p2.reset(p3.release());//reset釋放了p2原來指向的內存
傳遞unique_ptr參數和返回unique_ptr
不能拷貝uniqque_ptr 的規則有一個例外:我們可以拷貝或者賦值一個將要被
銷燬的unique_ptr.最常見的例子是從函數返回一個unique_ptr:
unique_ptr<int> clone(int p){
return unique_ptr<int>(new int(p));
}
unique_ptr<int> clone(int p){
unique_ptr<int> ret (new int(p));
return ret;
}
向unique_ptr中傳遞刪除器
//p指向一個類型爲objT的對象,並使用一個類型爲delT的對象釋放objT對象
//它會調用一個名爲fcn的delT類型對象
unique_ptr<objT,delT> p(new objT,fcn):
更具體的例子如下:
void f(destination &d/*其他參數*/){
connection c= connect(&d);
unique_ptr<connection,decltype(end_connection) *> p(&c,end_connection);
//其他代碼
}
12.1.6 weak_ptr
weak_ptr是一種不控制所指對象生命週期的智能指針,它指向由一個shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數。
一旦最後一個指向對象的shared_ptr被銷燬,對象就會被釋放。
下圖是weak_ptr的常見操作
當創建一個weak_ptr時,要用一個shared_ptr來初始化
auto p = make_shared<int>(42);
weak_ptr<int> wp(p);//wp弱共享p,p的引用計數未改變
因爲對象可能不存在,所以不能直接通過weak_ptr來訪問對象,需要調用其lock函數。如果存在,這個函數返回一個shared_ptr對象,然後使用這個對象來訪問。
if(shared_ptr<int> np = wp.lock()){
//np和p共享同一個對象
}
12.2 動態數組
new還可以一次分配多個對象。
12.2.1 new和數組
int *pia = new int[get_size()];
爲了分配一個數組,需要在類型名後面跟上一個方括號,在方括號內寫上,需要的個數,成功返回第一個對象的地址。
typedef int arrT[42];
int *p = new arrT;//分配一個含有42個元素的int數組
由上面可以看到,這種分配方式,返回的類型並不是數組類型,而是數組中元素類型的指針。
初始化動態分配對象的數組
int *pia = new int[10];
int *pia2 = new int[10]();//值初始化
string *psa = new string[10];
string *psa2 = new string[10]();//值初始化
還可以進行列表初始化
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
//前4個,用給定的值,進行初始化,剩下的進行值初始化
string *psa3 = new string[]{"a","an","the",string(3,'x')};
當 new[n] n等於0時,依然合法。
當new分配一個大小爲0的數組時,new返回一個合法的非空指針。此指針保證與new返回的其他任何指針都不相同。對於零長度的數組來說,此指針就像尾後指針一樣。
釋放動態數組
在delete後面加上方括號
delete p;//p必須指向一個動態分配的對象或爲空
delete [] pa; //pa必須指向一個動態分配的數組或爲空
第二條語句銷燬pa指向的數組中的元素,並釋放對應的內存。數組中的元素按照逆序進行銷燬。
上面的方括號是必須的,如果忽略方括號,行爲是未定義的。
typedef int arrT[42];
int *p = new arrT;
delete [] p;//此處的方括號也不能省略
智能指針和動態數組
unique_ptr
unique_ptr<int[] > up(new int[10]);
up.release();//自動調用delete[] 銷燬其指針
當unique_ptr指向數組的時候,不能直接使用點運算符和尖頭運算符。但是她能夠使用下標運算符。
下圖給出了支持的操作。
shared_ptr
shared_ptr不支持動態數組,如果要使用動態數組,必須自定義刪除器。
shared_ptr<int> sp(new int[10],[](int *p){delete[] p;});
sp.reset();//使用我們提供的lambada釋放數組,它使用delete[]
如果上面沒有提供刪除器,那麼結果將是未定義的。
如果要訪問此種的元素,下面是一個示例:
for(size_t i = 0;i != 10;++i){
*(sp.get() + i ) = i;//使用get獲取內置指針
}
12.2.2 allocator類
new將內存分配和對象構造組合在了一起,有時候,我們希望這兩者能夠分開,此時可以使用allocator類。比如下面的例子,就不需要將內存分配和對象構造組合在一起
string *const p = new string[n];
string s;
string *q = p;
while(cin >> s && q != p +n)
*q++ = s;
const size_t size = q -p ;
deletep[] p;
上面例子進行了兩處賦值:1.new 數組時,2.while循環體中讀取到數據之後。
allocator類
allocator分配的內存是一種,原始的,未構造的。下表列出了allocator的操作
allocator分配未構造的內存
allocator<string> alloc; //可以分配string的allocator對象
auto const p = alloc.allocate(n);//分配n個未初始化的string
allocator在未初始化的內存中構造對象
allocator分配完內存之後,此時可以按照需要進行對象的構造。
auto q = p;
alloc.construct(q++);//*q爲空字符串
alloc.construct(q++,10,'c');//*q爲cccccccccc
alloc.construct(q++,"hi");//*q爲hi
//q指向最後構造元素之後的位置
construct接受一個指向未初始化內存的指針,和零個或者多個額外的參數,這些參數,必須和需要構造的對象的某一個構造函數的參數類型相匹配。他們跟make_shared的參數類似。
cout << *p << endl;//正確:使用string的輸出運算符
cout << *q << endl;//錯誤:q指向未構造的內存
注意:爲了使用allocate返回的內存,必須使用construct構造對象。使用未構造的內存,其行爲是未定義的。
當使用完對象之後,必須對每個構造的對象調用destroy來銷燬他們。函數destroy接受一個指針,這個指針指向構造的對象。。
while(q!=p)
alloc.destroy(--q);
注意:只能對真正構造了的元素進行destroy操作
一旦destroy完成之後,就可以再次使用allocate方法,進行構造對象。
釋放未初始化的內存,需要調用deallocate函數,此函數,將未初始化的內存,返回給系統
alloc.deallocate(p,n);
傳遞給deallocate的p是allocated返回的值,n爲allocated傳遞進去的值。
拷貝和填充未初始化內存的算法
算法如下表:
例子如下:
auto p = alloc.allocate(vi.size() *2);
auto q = uninitialized_copy(vi.begin(),vi.end(),p);
uninitialized_fill_n(q.vi.size(),42);
本章完