第12章:動態(tài)內(nèi)存

  • #1.動態(tài)內(nèi)存與智能指針
    • 1.1 shared_ptr類
    • 1.2 直接管理內(nèi)存
    • 1.3 shared_ptr和new結(jié)合使用
    • 1.4 智能指針和異常
    • 1.5 unique_ptr
    • 1.6 weak_ptr
  • #2. 動態(tài)數(shù)組
    • 2.1 new和數(shù)組
    • 2.2 allocator類

除了自動和static對象外,C++還支持動態(tài)分配對象。動態(tài)分配的對象的生命周期與它們在哪里創(chuàng)建是無關(guān)的,只有顯示地被釋放時,這些對象才會銷毀。

動態(tài)對象的正確釋放被證明是編程中極其容易出錯的地方。為了更安全地使用動態(tài)對象,標(biāo)準(zhǔn)庫定義了兩個智能指針類型來管理動態(tài)分配的對象。當(dāng)一個對象應(yīng)該被釋放時,指向它的智能指針可以確保自動地釋放它。

靜態(tài)內(nèi)存用來保存局部static對象、類static數(shù)據(jù)成員以及定義在任何函數(shù)之外的變量。棧內(nèi)存用來保存保存定義在函數(shù)內(nèi)的非static對象。分配在靜態(tài)或棧內(nèi)存中的對象由編譯器自動創(chuàng)建和銷毀。對于棧對象,僅在其定義的程序塊運行時才存在;static對象在使用之前分配,在程序結(jié)束時銷毀。

除了靜態(tài)內(nèi)存和棧內(nèi)存,每個程序還擁有一個內(nèi)存池。這部分內(nèi)存被稱作自由空間。程序用堆來存儲動態(tài)分配的對象——即,那些在程序運行時分配的對象。

==雖然動態(tài)內(nèi)存有時是必要的,但眾所周知,正確地管理動態(tài)內(nèi)存是非常棘手的。==

#.1 動態(tài)內(nèi)存和智能指針

C++中,動態(tài)內(nèi)存管理是通過一對運算符來完成的:new,在動態(tài)內(nèi)存中為對象分配空間并返回一個指向該對象的指針,我們可以選擇對對象進行初始化;delete,接受一個動態(tài)對象的指針,銷毀該對象,并釋放與之關(guān)聯(lián)的內(nèi)存。

為了更容易地使用動態(tài)內(nèi)存,新的標(biāo)準(zhǔn)庫提供了兩種智能指針類型來管理動態(tài)對象。新的標(biāo)準(zhǔn)庫提供的這兩種智能指針的區(qū)別在于管理底層指針的方式:shared_ptr允許多個指針指向同一個對象;unique_ptr則“獨占”所指向的對象。標(biāo)準(zhǔn)庫還定義了一個名為weak_ptr的伴隨類,它是一種弱引用,指向shared_ptr所管理的對象。

1.1 shared_ptr類

類似vector,智能指針也是模板。因此,當(dāng)我們創(chuàng)建一個智能指針時,必須提供額外的信息——指針可以指向的類型。與vector一樣,我們在尖括號內(nèi)給出類型,之后是所定義的這種智能指針的名字:

shared\_ptr<string> p1;//shared_ptr,可以指向string
shared\_ptr<list<string>> p2; //shared_ptr,可以指向int的list

默認(rèn)初始化的智能指針保存著一個空指針。

智能指針的使用方式與普通指針類似。解引用一個智能指針返回它指向的對象。如果在一個條件判斷中使用智能指針,效果就是檢測它是否為空:

if(p1 && p1->empty()) {
    *p1 = "hi"; //如果p1指向一個空string,解引用p1,將一個新值賦予string
}
make_shared函數(shù)

最安全的分配和使用動態(tài)內(nèi)存的方法是調(diào)用一個名為make_shared的標(biāo)準(zhǔn)庫函數(shù)。此函數(shù)在動態(tài)內(nèi)存中分配一個對象并初始化它,返回指向此對象的shared_ptr。

當(dāng)要用make_shared時,必須指定想要創(chuàng)建的對象的類型。定義方式與模板類相同,在函數(shù)名之后跟一個尖括號,在其中給出類型:

//指向一個值為42個int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
//p1指向一個值為"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
//p5指向一個值初始化的int,即,值為0
shared_ptr<int> p5 = make_shared<int>();

類似順序容器的emplace成員,make_shared用其參數(shù)來構(gòu)造給定類型的對象。

shared_ptr的拷貝和賦值

當(dāng)進行拷貝或賦值操作時,每個shared_ptr都會記錄有多少個其他shared_ptr指向相同的對象:

auto p = make_shared<int>(42);  //p指向的對象只有p一個引用者
auto q(p);  //p和q指向相同的對象,此對象有兩個引用者

我們可以認(rèn)為每個shared_ptr都有一個關(guān)聯(lián)的計數(shù)器,通常稱其為引用計數(shù)。無論何時我們拷貝一個shared_ptr,計數(shù)器都會遞增。例如,當(dāng)用一個shared_ptr初始化另一個shared_ptr,或?qū)⑺鳛閰?shù)傳遞給一個函數(shù)以及作為函數(shù)的返回值時,它所關(guān)聯(lián)的計數(shù)器就會遞增。當(dāng)我們給shared_ptr賦予一個新值或是shared_ptr被銷毀(例如一個局部的shared_ptr)離開其作用域時,計數(shù)器就會遞減。

一旦一個shared_ptr的計數(shù)器變?yōu)?,它就會自動釋放自己所管理的對象:

auto r = make_shared<int>(42); //r指向的int只有一個引用者
r = q; //給r賦值,令它指向另一個地址
       //遞增q指向的對象的引用計數(shù)
       //遞減r原來指向?qū)ο蟮囊糜嫈?shù)
       //r原來指向的對象已沒有引用者,會自動釋放

此例中我們分配了一個int,將其指針保存在r中。接下來,我們將一個新值賦予r。在此情況下,r是唯一指向此int的shared_ptr,在把q賦給r的過程中,此int被自動釋放。

使用了動態(tài)生存期的資源的類

程序使用動態(tài)內(nèi)存出于以下三種原因之一:

  1. 程序不知道自己需要使用多少對象
  2. 程序不知道所需對象的準(zhǔn)確類型
  3. 程序需要多個對象間共享數(shù)據(jù)

==使用動態(tài)內(nèi)存的一個常見原因是允許多個對象共享相同的狀態(tài)。==

1.2 直接管理內(nèi)存

C++語言定義了兩個運算符來分配和釋放動態(tài)內(nèi)存。運算符new分配內(nèi)存,delete釋放new分配的內(nèi)存。

使用new動態(tài)分配和初始化對象

在自由空間分配的內(nèi)存是無名的,因此new無法為其分配的對象命名,而是返回一個指向該對象的指針:

int* p1 = new int;//p1指向一個動態(tài)分配的、未初始化的無名對象

此new表達(dá)式在自由空間構(gòu)造一個int型對象,并返回指向該對象的指針。

默認(rèn)情況下,動態(tài)分配的對象是默認(rèn)初始化的,這意味著內(nèi)置類型或組合類型的對象的值將是未定義的,而類類型對象將用默認(rèn)構(gòu)造函數(shù)進行初始化:

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

我們可以使用直接初始化方式來初始化一個動態(tài)分配的對象。我們可以使用傳統(tǒng)的構(gòu)造方式(使用圓括號),在新標(biāo)準(zhǔn)下,也可以使用列表初始化(使用花括號):

int *pi = new int(1024); //pi指向的對象的值為1024
string *ps = new string(10,'9'); //*ps為“9999999999”
//vector有10個元素,值依次從0到9
vector<int> *pv = new vector<int>{0,1,2,3,4,5,6,7,8,9};

也可以對動態(tài)分配的對象進行值初始化,只需在類型名之后跟一對花括號即可:

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

==出于與變量初始化相同的原因,對動態(tài)分配的對象進行初始化通常是個好主意。==

動態(tài)分配的const對象

用new分配const對象是合法的:

//分配并初始化一個const int
const int *pci = new const int(1024);//分配并初始化一個const int
//分配并默認(rèn)初始化一個const的空string
const string *pcs = new const string;//分配并默認(rèn)初始化一個const的空string

類似其他任何const對象,一個動態(tài)分配的const對象必須進行初始化。對于一個定義了默認(rèn)構(gòu)造函數(shù)的類類型,其const動態(tài)對象可以隱式初始化,而其他類型的對象就必須顯示初始化。由于默認(rèn)分配的對象是const的,new返回的指針是一個指向const的指針。

內(nèi)存耗盡

一旦一個程序用光了它所有可用的內(nèi)存,new表達(dá)式就會失敗。默認(rèn)情況下,如果new不能分配所要求的內(nèi)存空間,它會拋出一個類型為bad_alloc的異常。我們可以改變使用new的方式來阻止它拋出異常:

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

我們稱這種形式的new為定位new。定位new表達(dá)式允許我們向new傳遞額外的參數(shù)。在此例中,我們傳遞給它一個由標(biāo)準(zhǔn)庫定義的名為nothrow的對象。如果將nothrow傳遞給new,我們的意圖是告訴它不能拋出異常。如果這種形式的new不能分配所需內(nèi)存,它會返回一個空指針。bad_alloc和nothrow都定義在頭文件new中。

釋放動態(tài)內(nèi)存

為了防止內(nèi)存耗盡,在動態(tài)內(nèi)存使用完畢后,必須歸還給系統(tǒng)。我們通過delete表達(dá)式來將動態(tài)內(nèi)存歸還給系統(tǒng)。delete表達(dá)式接受一個指針,指向我們想要釋放的對象:

delete p; //p必須指向一個動態(tài)分配的對象或是一個空指針

與new類型類似,delete表達(dá)式也執(zhí)行兩個動作:銷毀給定指針指向的對象;釋放對應(yīng)的內(nèi)存。

指針值和delete

我們傳遞給delete的指針必須指向動態(tài)分配的內(nèi)存,或者是一個空指針。釋放一塊并非new分配的內(nèi)存,或者將相同的指針值釋放多次,其行為是未定義的:

int i, *pi1 = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; //錯誤:i不是一個指針
delete pi1; //未定義:pi1指向一個局部變量
delete pd; //正確
delete pd2; //未定義:pd2指向的內(nèi)存已經(jīng)被釋放了
delete pi2; //正確:釋放一個空指針總是沒有錯誤的
動態(tài)對象的生存期直到被釋放時為止

由shared_ptr管理的內(nèi)存在最后一個shared_ptr銷毀時會被自動釋放。但對于通過內(nèi)置指針類型來管理的內(nèi)存,就不是這樣了。對于一個由內(nèi)置指針管理的動態(tài)對象,直到被顯示釋放之前它都是存在的。

返回指向動態(tài)內(nèi)存的指針(而不是智能指針)的函數(shù)給其調(diào)用者增加了一個額外負(fù)擔(dān)——調(diào)用者必須記得釋放內(nèi)存:

//factory返回一個指針,指向一個動態(tài)分配的對象
Foo* factory(T arg) {
    return new Foo(arg);//調(diào)用者負(fù)責(zé)釋放此內(nèi)存
}

此處,use_factory函數(shù)調(diào)用factory,后者分配一個類型為Foo的新對象。當(dāng)use_factory返回時,局部變量p被銷毀。此變量是一個內(nèi)置指針,而不是一個智能指針。

==由內(nèi)置指針(而不是智能指針)管理的動態(tài)內(nèi)存在被顯式釋放前一直都會存在。==

delete之后重置指針值

在delete之后,指針就變成了人們所說的空懸指針,即,指向一個塊曾經(jīng)保存數(shù)據(jù)對象但現(xiàn)在已經(jīng)無效的內(nèi)存的指針。

int *p(new int(42)); //p指向動態(tài)內(nèi)存
auto q = p; //p和q指向相同的內(nèi)存
delete p; //p和q均變?yōu)闊o效
p = nullptr; //指出p不再綁定到任何對象

重置p對q沒有任何作用,在我們釋放p所指向的內(nèi)存時,q也變?yōu)闊o效了。

1.3 shared_ptr和new結(jié)合使用

如前所述,如果我們不初始化一個智能指針,它就會被初始化為一個空指針。我們可以用new返回的指針來初始化智能指針:

shared_ptr<double> p1; //shared_ptr可以指向一個double
shared_ptr<int> p2(new int(42)); //p2指向一個值為42的int

接受指針參數(shù)的智能指針構(gòu)造函數(shù)是explicit的。因此,不能將一個內(nèi)置指針隱式轉(zhuǎn)換為一個智能指針,必須使用直接初始化形式。

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

p1的初始化隱式地要求編譯器用一個new返回的int*來創(chuàng)建一個shared_ptr。由于我們不能進行內(nèi)置指針到智能指針的隱式轉(zhuǎn)換,因此這條初始化語句是錯誤的。出于相同的原因,一個返回shared_ptr的函數(shù)不能在其返回語句中隱式轉(zhuǎn)換一個普通指針:

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

我們必須將shared_ptr顯式綁定到一個想要返回的指針上:

shared_ptr<int> clone(int p) {
    //正確:顯式地用int*創(chuàng)建shared_ptr<int>
    return new shared_ptr(new int(p));
}

默認(rèn)情況下,一個用來初始化智能指針的普通指針必須指向動態(tài)內(nèi)存,因為智能指針默認(rèn)使用delete釋放它所關(guān)聯(lián)的對象。

不要混合使用普通指針和智能指針......

shared_ptr可以協(xié)調(diào)對象的析構(gòu),但這僅限于其自身的拷貝(也是shared_ptr)之間。這也是為什么我們推薦使用make_shared而不是new的原因。這樣,我們就能在分配對象的同時將shared_ptr與之綁定,從而避免了無意中將同一塊內(nèi)存綁定到多個獨立創(chuàng)建的shared_ptr上。

考慮下面對shared_ptr進行操作的函數(shù):

void process(shared_ptr<int> ptr) {
    //使用ptr
}//ptr離開作用域,被銷毀

process的參數(shù)是傳值方式傳遞的,因此實參會被拷貝到ptr中??截愐粋€shared_ptr會遞增其引用計數(shù),因此,在process運行過程中,引用計數(shù)至少為2。當(dāng)process接受時,ptr的引用計數(shù)會遞減,但不會為0。因此,當(dāng)局部變量ptr被銷毀時,ptr指向的內(nèi)存不會被釋放。

使用此函數(shù)的正確方法是傳遞給它一個shared_ptr:

shared_ptr<int> p(new int(42)); //引用計數(shù)為1
process(p); //拷貝p會遞增它的引用計數(shù);在process中引用計數(shù)值為2
int i = *p; //正確:引用計數(shù)值為1

雖然不能傳遞給process一個內(nèi)置指針,但可以傳遞給它一個(臨時的)shared_ptr,這個shared_ptr是用一個內(nèi)置指針顯式構(gòu)造的。但是,這樣做很肯能會導(dǎo)致錯誤:

int *x(new int(1024)); //危險:x是一個普通指針,不是一個智能指針
process(x); //錯誤:不能講int*轉(zhuǎn)換為一個shared_ptr<int>
process(shared_ptr<int>(x)); //合法,但內(nèi)存會被釋放
int j = *x; //未定義的:x是一個空懸指針

當(dāng)將一個shared_ptr綁定到一個普通指針時,我們就將內(nèi)存管理責(zé)任交給了這個shared_ptr。一旦這樣做了,我們就不應(yīng)該使用內(nèi)置指針來訪問shared_ptr所指向的內(nèi)存。

==使用內(nèi)置指針來訪問一個智能指針?biāo)?fù)責(zé)的對象是很危險的,因為我們無法知道對象何時被銷毀==。

......也不要使用get初始化另一個智能指針或為智能指針賦值

智能指針定義了一個名為get的函數(shù),它返回一個內(nèi)置指針,指向智能指針管理的對象。此函數(shù)是為了這樣一種情況而設(shè)計的:我們需要向不能使用智能指針的代碼傳遞一個內(nèi)置指針。使用get返回的指針的代碼不能delete此指針。

雖然編譯器不會給出錯誤信息,但將另一個智能指針也綁定到get返回的指針上是錯誤的:

shared_ptr<int> p(new int(42)); //引用計數(shù)為1
int *q = p.get(); //正確:但使用q時要注意,不要讓它管理的指針被釋放
{//新程序塊
    //未定義:兩個獨立的shared_ptr指向相同的內(nèi)存
    shared_ptr<int>(q);
}//程序塊結(jié)束,q被銷毀,它指向的內(nèi)存被釋放
int foo = *p;

在本例中,p和q指向相同的內(nèi)存。由于它們是相互獨立創(chuàng)建的,因此各自的引用計數(shù)都是1。當(dāng)q所在的程序塊結(jié)束時,q被銷毀,這會導(dǎo)致q指向的內(nèi)存被釋放。從而p變成一個空懸指針,意味著當(dāng)我們試圖使用p時,將發(fā)生未定義的行為。而且,當(dāng)p被銷毀時,這塊內(nèi)存會被第二次delete。

==get用來將指針的訪問權(quán)限傳遞給代碼,你只有在確定代碼不會delete指針的情況下,才能使用get。特別是,永遠(yuǎn)不要用get初始化另一個智能指針或者為另一個智能指針賦值。==

其他shared_ptr操作

shared_ptr還定義了其他一些操作,我們可以用reset來將一個新的指針賦予一個shared_ptr:

p = new int(1024); //錯誤:不能將一個指針賦予shared_ptr
p.reset(new int(1024)); //正確:p指向一個新對象

與賦值類似,reset會更新引用計數(shù),如果需要的話,會釋放p指向的對象。reset成員經(jīng)常與unique一起使用,來控制多個shared_ptr共享的對象。在改變底層對象之前,我們檢查自己是否是當(dāng)前對象僅有的用戶。如果不是,在改變之前要制作一份新的拷貝:

if(!p.unique()) {
    p.reset(new string(*p)); //我們不是唯一的用戶;分配新的拷貝
}
*p += newVal; //現(xiàn)在我們知道自己是唯一的用戶,可以改變對象的值

1.4 智能指針和異常

使用異常處理的程序能在異常發(fā)生后令程序流程繼續(xù),我們注意到,這種程序需要確保在異常發(fā)生后資源能被正確的釋放。一個簡單的確保資源被釋放的方法是使用智能指針。

如果使用智能指針,即使程序塊過早結(jié)束,智能指針類也能確保在內(nèi)存不再需要時將其釋放:

void f() {
    shared_ptr<int> sp(new int(42));//分配一個對象
    //這段代碼拋出一個異常,且在f中未被捕獲
}//在函數(shù)結(jié)束時shared_ptr自動釋放內(nèi)存

與之相對的,當(dāng)發(fā)生異常時,我們直接管理的內(nèi)存是不會自動釋放的。如果使用內(nèi)置指針管理內(nèi)存,且在new之后在對應(yīng)的delete之前發(fā)生了異常,則內(nèi)存不會被釋放:

void f() {
    int *ip = new int(42); //動態(tài)分配一個新對象
    //這段代碼拋出一個異常,且在f中未被捕獲。
    delete ip;  //在退出之前釋放內(nèi)存
}

如果在new和delete之間發(fā)生異常,且異常未在f中被捕獲,則內(nèi)存就永遠(yuǎn)不會被釋放了。在函數(shù)f之外沒有指針指向這塊內(nèi)存,因此就無法釋放它了。

注意:智能指針陷阱
  • 不使用相同的內(nèi)置指針值初始化(或reset)多個智能指針。
  • 不delete get()返回的指針。
  • 不適用get()初始化或reset另一個智能指針。
  • 如果你使用get()返回的指針,記住當(dāng)最后一個對應(yīng)的智能指針銷毀后,你的指針就變?yōu)闊o效了。
  • 如果你使用智能指針管理的資源不是new分配的內(nèi)存,記住傳遞給它一個刪除器(deleter)。

1.5 unique_ptr

一個unique_ptr“擁有”它所指向的對象。與shared_ptr不同,某個時刻只能有一個unique_ptr指向一個給定對象。當(dāng)unique_ptr被銷毀時,它所指向的對象也被銷毀。我們不能拷貝和賦值unique_ptr。

與shared_ptr不同,沒有類似make_shared的標(biāo)準(zhǔn)庫函數(shù)返回一個unique_ptr。當(dāng)我們定義一個unique_ptr時,需要將其綁定到一個new返回的指針上。類似shared_ptr,初始化unique_ptr必須采用直接初始化形式:

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;
p3 = p2; //錯誤:unique_ptr不支持賦值

雖然我們不能拷貝或賦值unique_ptr,但可以通過調(diào)用release或reset將指針的所有權(quán)從一個(非const)unique_ptr轉(zhuǎn)移給另一個unique:

unique_ptr<string> p1(new string("Stegosaurus"));
//將所有權(quán)從p1轉(zhuǎn)移給p2
unique_ptr<string> p2(p1.release()); //release將p1置空
unique_ptr<string> p3(new string("Trex"));
//將所有權(quán)從p3轉(zhuǎn)移給p2
p2.reset(p3.release()); //reset釋放了p2原來指向的內(nèi)存

release成員返回unique_ptr當(dāng)前保存的指針并將其置為空。因此,p2被初始化為p1原來保存的指針,而p1置為空。

reset成員接受一個可選的指針參數(shù),令unique_ptr重新指向給定的指針。如果unique_ptr不為空,它原來指向的對象被釋放。因此,對p2調(diào)用reset釋放了用“Stegosaurus”初始化的string所使用的內(nèi)存,將p3對指針的所有權(quán)轉(zhuǎn)移給p2,并將p3置為空。

調(diào)用release會切斷unique_ptr和它原來管理的對象間的聯(lián)系。release返回的指針通常被用來初始化另一個智能指針或給另一個智能指針賦值。在本例中,管理內(nèi)存的責(zé)任簡單地從一個智能指針轉(zhuǎn)移到另一個。但是,如果我們不用另一個智能指針來保存release返回的指針,我們的程序就要負(fù)責(zé)資源的釋放:

p2.release(); //錯誤:p2不會釋放內(nèi)存,而且我們丟失了指針
auto p = p2.release(); //正確,但我們必須記得delete(p)
傳遞unique_ptr參數(shù)和返回unique_ptr

不能拷貝unique_ptr的規(guī)則有一個例外:我們可以拷貝和賦值一個將要被銷毀的unique_ptr。最常見的例子是從函數(shù)返回一個unique_ptr。

unique_ptr<int> clone(int p) {
    //正確:從int*創(chuàng)建一個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;
}

對于兩段代碼,編譯器都知道要返回的對象將要被銷毀。在此情況下,編譯器執(zhí)行一種特殊的“拷貝”。

向unique_ptr傳遞刪除器

類似shared_ptr,unique_ptr默認(rèn)情況下用delete釋放它指向的對象。與shared_ptr一樣,我們可以重載一個unique_ptr中默認(rèn)的刪除器。但是,unique_ptr管理刪除器的方式與shared_ptr不同。

重載一個unique_ptr中的刪除器會影響到unique_ptr類型以及如何構(gòu)造(或reset)該類型的對象。與重載關(guān)聯(lián)容器的比較操作類似,我們必須在尖括號中unique_ptr指向類型之后提供刪除器類型。在創(chuàng)建或reset一個這種unique_ptr類型的對象時,必須提供一個指定類型的可調(diào)用對象(刪除器):

//p指向一個類型為objT的對象,并使用一個類型為delT的對象釋放objT對象
//它會調(diào)用一個名為fcn的delT類型對象
unique_ptr<objT,delT> p(new objT,fcn);

1.6 weak_ptr

weak_ptr是一種不控制所指向?qū)ο笊嫫诘闹悄苤羔槪赶蛴梢粋€shared_ptr管理的對象。將一個weak_ptr綁定到一個shared_ptr不會改變shared_ptr的引用計數(shù)。一旦最后一個指向?qū)ο蟮膕hared_ptr被銷毀,對象就會被釋放。即使有weak_ptr指向?qū)ο?,對象也還是會被釋放,因此,weak_ptr的名字抓住了這種智能指針“弱”共享對象的特點。

當(dāng)我們創(chuàng)建一個weak_ptr時,要用一個shared_ptr來初始化它:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p); //wp弱共享p;p的引用計數(shù)未改變

本例中wp和p指向相同的對象。由于是弱共享,創(chuàng)建wp不會改變p的引用計數(shù);wp指向的對象可能被釋放掉。

由于對象可能不存在,我們不能使用weak_ptr直接訪問對象,而必須調(diào)用lock。此函數(shù)檢查weak_ptr指向的對象是否仍存在。如果存在,lock返回一個指向共享對象的shared_ptr。與任何其他shared_ptr類似,只要此shared_ptr存在,它所指向的底層對象也就會一直存在。例如:

if (shared_ptr<int> np = wp.lock()) //np不為空則條件成立
{
    //在if中,np與p共享對象
}

#2. 動態(tài)數(shù)組

new和delete運算符一次分配/釋放一個對象,但某些應(yīng)用需要一次為很多對象分配內(nèi)存的功能。例如,vector和string都是在連續(xù)內(nèi)存中保存它們的元素,因此,當(dāng)容器需要重新分配內(nèi)存時,必須一次性為很多元素分配內(nèi)存。

為了支持這種需求,C++語言和標(biāo)準(zhǔn)庫提供了兩種一次分配一個對象數(shù)組的方法。C++語言定義了另一種new表達(dá)式語法,可以分配并初始化一個對象數(shù)組。標(biāo)準(zhǔn)庫中包含一個名為allocator的類,允許我們將分配和初始化分離。使用allocator通常會提供更好的性能和更靈活的內(nèi)存管理能力。

==大多數(shù)應(yīng)用應(yīng)該使用標(biāo)準(zhǔn)庫容器而不是動態(tài)分配的數(shù)組。使用容器更為簡單、更不容易出現(xiàn)內(nèi)存管理錯誤并且可能有更好的性能。==

2.1 new和數(shù)組

為了讓new分配一個對象數(shù)組,我們要在類型名之后跟一對方括號,在其中指明要分配的對象的數(shù)目。在下例中,new分配要求數(shù)量的對象并(假定分配成功后)返回指向第一個對象的指針:

//調(diào)用get_size()確定分配多少個int
int *pia = new int[get_size()]; //pia指向第一個int

方括號中大小必須是整型,但不必是常量。

也可以用一個表示數(shù)組類型的類型別名來分配一個數(shù)組,這樣,new表達(dá)式就不需要方括號了:

typedef int arrT[42]; //arrT表示42個int的數(shù)組類型
int *p = new arrT; //分配一個42個int的數(shù)組;p指向第一個int

在本例中,new分配一個int數(shù)組,并返回指向第一個int的指針。即使這段代碼中沒有方括號,編譯器執(zhí)行這個表達(dá)式還是會用new[]。即,編譯器執(zhí)行如下形式:

int *p = new int[42];
分配一個數(shù)組得到一個元素類型的指針

當(dāng)用new分配一個數(shù)組時,我們并未得到一個數(shù)組類型的對象,而是得到一個數(shù)組元素類型的指針。即使我們使用類型別名定義一個數(shù)組類型,new也不會分配一個數(shù)組類型的對象。

==要記住我們所說的動態(tài)數(shù)組并不是數(shù)組類型,這是很重要的。==

初始化動態(tài)分配對象的數(shù)組

默認(rèn)情況下,new分配的對象,不管是單個分配還是數(shù)組中的,都是默認(rèn)初始化的。可以對數(shù)組中的元素進行值初始化,方法是在大小之后跟一對空括號。

int *pia = new int[10]; //10個未初始化的int
int *pia2 = new int[10](); //10個值初始化為0的int
string *psa = new string[10]; //10個空string
string *psa2 = new string[10](); //10個空string

在新標(biāo)準(zhǔn)中,我們還可以提供一個元素初始化器的花括號列表:

//10個int分別用列表對應(yīng)的初始化器初始化
int *pia3 = new int[10]{0,1,2,3,4,5,6,7,8,9};
//10個string,前4個用給定的初始化器初始化,剩余的進行值初始化
string *psa3 = new string[10]{ "a","an","the",string(3,'x')};

與內(nèi)置數(shù)組對象的列表初始化一樣,初始化器會用來初始化動態(tài)數(shù)組中開始部分的元素。如果初始化器數(shù)目小于元素數(shù)目,剩余元素將進行值初始化。如果初始化器數(shù)目大于元素數(shù)目,則new表達(dá)式失敗,不會分配任何內(nèi)存。

動態(tài)分配一個空數(shù)組是合法的

可以用任意表達(dá)式來確定要分配的對象的數(shù)目:

size_t n = get_size(); //get_size返回需要的元素的數(shù)目
int *p = new int[n]; //分配數(shù)組保存元素
for(int *q = p;q != p + n;++q) {
    /*處理數(shù)組*/
}

這樣產(chǎn)生了一個有意思的問題:如果get_size返回0,會發(fā)生什么?答案是代碼仍能正常工作。雖然我們不能創(chuàng)建一個大小為0的靜態(tài)數(shù)組對象,但當(dāng)n等于0時,調(diào)用new[n]是合法的:

char arr[0]; //錯誤:不能定義長度為0的數(shù)組
char *cp = new char[0]; //正確:但cp不能解引用

當(dāng)我們用new分配一個大小為0的數(shù)組時,new返回一個合法的非空指針。此指針保證與new返回的其他任何指針都不相同。對于零長度的數(shù)組來說,此指針就像尾后指針一樣。

釋放動態(tài)數(shù)組

為了釋放動態(tài)數(shù)組,我們使用一種特殊形式的delete——在指針前加一個空方括號對:

delete p; //p必須指向一個動態(tài)分配的對象或為空
delete[] pa; //pa必須指向一個動態(tài)分配的數(shù)組或為空

第二條語句銷毀pa指向的數(shù)組中元素,并釋放對應(yīng)的內(nèi)存。數(shù)組中的元素按逆序銷毀,即,最后一個元素首先被銷毀,然后是倒數(shù)第二個,依此類推。

當(dāng)我們釋放一個指向數(shù)組的指針時,空方括號對是必需的:它指示編譯器此指針指向一個對象數(shù)組的第一個元素。如果我們在delete一個指向數(shù)組的指針時忽略了方括號,其行為是未定義的。

回憶一下,當(dāng)我們使用一個類型別名來定義一個數(shù)組類型時,在new表達(dá)式中不使用[]。即使是這樣,在釋放一個數(shù)組指針時也必須使用方括號:

typedef int arrT[42]; //arrT是42個int數(shù)組的類型別名
int *p = new arrT; //分配一個42個int的數(shù)組;p指向第一個元素
delete[] p; //方括號是必需的,因為我們當(dāng)初分配的是一個數(shù)組

不管外表如何,p指向一個對象數(shù)組的首元素,而不是一個類型為arrT的單一對象。因此,在釋放p時我們必須使用[]。

智能指針和動態(tài)數(shù)組

標(biāo)準(zhǔn)庫提供了一個可以管理new分配的數(shù)組的unique_ptr版本。為了用一個unique_ptr管理動態(tài)數(shù)組,我們必須在對象類型后面跟一對空方括號:

unique_ptr<int[]> up(new int[10]); //up指向一個包含10個未初始化int的數(shù)組
up.release(); //自動用delete[]銷毀其指針

類型說明符中的方括號(<int[]>)指出up指向一個int數(shù)組而不是一個int。由于up指向一個數(shù)組,當(dāng)up銷毀它管理的指針時,會自動使用delete[]。

2.2 allocator類

new有一些靈活性上的局限,其中一方面表現(xiàn)在它將內(nèi)存分配和對象構(gòu)造組合在一起。類似的,delete將對象析構(gòu)和內(nèi)存釋放組合在一起。

當(dāng)分配一大塊內(nèi)存時,我們通常計劃在這塊內(nèi)存上按需構(gòu)造對象。在此情況上,我們希望將內(nèi)存分配和對象構(gòu)造分離。這意味著我們可以分配大塊內(nèi)存,但只在真正需要時才真正執(zhí)行對象創(chuàng)建操作。

一般情況下,將內(nèi)存分配和對象構(gòu)造組合在一起可能導(dǎo)致不必要的浪費。例如:

string *const p = new string[n]; //構(gòu)造n個空string
string s;
string *q = p; //p指向第一個string
while(cin >> s && q != p + n) {
    *q++ = s; //賦予*q一個新值
}
const size_t size = q - p; //記住我們讀取了多少個string
//使用數(shù)組
delete[] p; //p指向一個數(shù)組;記得delete[]來釋放

new表達(dá)式分配并初始化了n個string。但是,我們可能不需要n個string,少量string可能就足夠了。這樣,我們就可能創(chuàng)建了一些永遠(yuǎn)也用不到的對象。

allocator類

標(biāo)準(zhǔn)庫allocator類定義在頭文件memory中,它幫助我們將內(nèi)存分配和對象構(gòu)造分離開來。它提供一種類型感知的內(nèi)存分配方法,它分配的內(nèi)存是原始的、未構(gòu)造的。

類似vector,allocator是一個模板。為了定義一個allocator對象,我們必須指明這個allocator可以分配的對象類型。當(dāng)一個allocator對象分配內(nèi)存時,它會根據(jù)給定的對象類型來確定恰當(dāng)?shù)膬?nèi)存大小和對齊位置:

allocator<string> alloc; //可以分配string的allocator對象
auto const p = alloc.allocate(n); //分配n個未初始化的string

這個allocate調(diào)用為n個string分配了內(nèi)存。

allocator分配未構(gòu)造的內(nèi)存

allocator分配的內(nèi)存是未構(gòu)造的。我們按需要在此內(nèi)存中構(gòu)造對象。在新標(biāo)準(zhǔn)庫中,construct成員函數(shù)接受一個指針和零個或多個額外參數(shù),在給定位置構(gòu)造一個元素。額外參數(shù)用來初始化構(gòu)造的對象。類似make_shared的參數(shù),這些額外參數(shù)必須是與構(gòu)造的對象的類型相匹配的合法的初始化器:

auto q = p; //q指向最后構(gòu)造的元素之后的位置
alloc.construct(q++); //*q為空字符串
alloc.construct(q++, 10, 'c');
alloc.construct(q++,"hi");

在早起版本的標(biāo)準(zhǔn)庫中,construct只接受兩個參數(shù):指向創(chuàng)建對象位置的指針和一個元素類型的值。因此,我們只能將一個元素拷貝到未構(gòu)造空間中,而不能用元素類型的任何其他構(gòu)造函數(shù)來構(gòu)造一個元素。

還未構(gòu)造對象的情況下就使用原始內(nèi)存是錯誤的:

cout << *p << endl; //正確:使用string的輸出運算符
cout << *q << endl; //災(zāi)難:q指向未構(gòu)造的內(nèi)存

==為了使用allocate返回的內(nèi)存,我們必須用construct構(gòu)造對象。使用未構(gòu)造的內(nèi)存,其行為是未定義的。==

當(dāng)我們使用完對象后,必須對每個構(gòu)造的元素調(diào)用destroy來銷毀它們。函數(shù)destory接受一個指針,對指向的對象執(zhí)行析構(gòu)函數(shù):

while (q != p)
{
    alloc.destroy(--q);//釋放我們真正構(gòu)造的string
}

在循環(huán)開始處,q指向最后構(gòu)造的元素之后的位置。我們在調(diào)用destroy之前對q進行了遞減操作。因此,第一次調(diào)用destroy時,q指向最后一個構(gòu)造的元素。最后一步循環(huán)中我們destroy了第一個構(gòu)造的元素,隨后q將與p相等,循環(huán)結(jié)束。

==我們只能對真正構(gòu)造了的元素進行destory操作==

一旦元素銷毀后,就可以重新使用這部分內(nèi)存來保存其他string,也可也將其歸還給系統(tǒng)。釋放內(nèi)存通過調(diào)用deallocate來完成:

alloc.deallocate(p,n);

我們傳遞給deallocate的指針不能為空,它必須指向由allocate分配的內(nèi)存。而且,傳遞給deallocate的大小參數(shù)必須與調(diào)用allocated分配內(nèi)存時提供的大小參數(shù)具有一樣的值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容