[C++ Primer Note11] 動(dòng)態(tài)內(nèi)存

除了自動(dòng)和static對(duì)象外,C++還支持動(dòng)態(tài)分配對(duì)象。動(dòng)態(tài)分配的對(duì)象的生存期與它們?cè)谀睦飫?chuàng)建是無關(guān)的,只有當(dāng)顯式地被釋放時(shí),這些對(duì)象才會(huì)銷毀。

  1. 靜態(tài)內(nèi)存用來保存局部static對(duì)象,類static數(shù)據(jù)成員,以及定義在任何函數(shù)之外的變量。棧內(nèi)存用來保存定義在函數(shù)內(nèi)的非static對(duì)象。分配在靜態(tài)內(nèi)存或棧內(nèi)存的對(duì)象由編譯器自動(dòng)創(chuàng)建和銷毀。
  2. 除了靜態(tài)內(nèi)存和棧內(nèi)存,每個(gè)程序還擁有一個(gè)內(nèi)存池。這部分內(nèi)存被稱作自由空間(free store)堆(heap)。程序用堆來存儲(chǔ)動(dòng)態(tài)分配的對(duì)象——即程序運(yùn)行時(shí)分配的對(duì)象。動(dòng)態(tài)對(duì)象的生存期由程序而不是編譯器來控制。
  3. C++中,動(dòng)態(tài)內(nèi)存的管理是通過一對(duì)運(yùn)算符來完成的:new,在動(dòng)態(tài)內(nèi)存中為對(duì)象分配空間并返回一個(gè)指向該對(duì)象的指針;delete,接受一個(gè)動(dòng)態(tài)對(duì)象的指針,銷毀該對(duì)象,并釋放與之關(guān)聯(lián)的內(nèi)存。
  4. 為了更容易,更安全地使用動(dòng)態(tài)內(nèi)存,新的標(biāo)準(zhǔn)庫提供了兩種智能指針(smart pointer)類型來管理動(dòng)態(tài)對(duì)象。智能指針的行為類似常規(guī)指針,重要的區(qū)別是它負(fù)責(zé)自動(dòng)釋放所指向的對(duì)象。shared_ptr允許多個(gè)指針指向同一個(gè)對(duì)象;unique_ptr則“獨(dú)占”所指向的對(duì)象。標(biāo)準(zhǔn)庫還定義了一個(gè)名為weak_ptr的伴隨類,它是一種弱引用,指向shared_ptr所管理的對(duì)象,這三種類型都定義在memory頭文件中。
  5. 類似vector,智能指針也是模板,因此當(dāng)我們創(chuàng)建智能指針時(shí),必須提供額外信息——指針可以指向的類型。
shared_ptr<string> p1;
shared_ptr<list<int>> p2;

默認(rèn)初始化的智能指針保存著一個(gè)空指針
智能指針的使用方式與普通指針類似,解引用返回它指向的對(duì)象,如果在條件判斷中使用智能指針,效果就是檢測(cè)它是否為空

image

  1. 最安全的分配和使用動(dòng)態(tài)內(nèi)存的方法是調(diào)用一個(gè)名為make_shared的標(biāo)準(zhǔn)庫函數(shù)。此函數(shù)在動(dòng)態(tài)內(nèi)存中分配一個(gè)對(duì)象并初始化它,返回指向此對(duì)象的shared_ptr。該函數(shù)同樣定義在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>();

如果我們不傳遞參數(shù),對(duì)象會(huì)進(jìn)行值初始化

  1. 當(dāng)進(jìn)行拷貝或賦值操作時(shí),每個(gè)shared_ptr都會(huì)記錄有多少個(gè)其他shared_ptr指向相同的對(duì)象:
auto p=make_shared<int>(42);
auto q(p);  //p和q指向相同對(duì)象,此對(duì)象有兩個(gè)引用者

我們可以認(rèn)為每個(gè)shared_ptr都有一個(gè)關(guān)聯(lián)的計(jì)數(shù)器,通常稱為引用計(jì)數(shù)(reference count),無論我們何時(shí)拷貝一個(gè)shared_ptr,計(jì)數(shù)器都會(huì)遞增。當(dāng)我們給shared_ptr賦予一個(gè)新值或是shared_ptr被銷毀(例如一個(gè)局部的shared_ptr離開作用域),計(jì)數(shù)器就會(huì)遞減。

  1. 當(dāng)指向一個(gè)對(duì)象的最后一個(gè)shared_ptr被銷毀時(shí),shared_ptr類會(huì)自動(dòng)銷毀此對(duì)象。它是通過析構(gòu)函數(shù)實(shí)現(xiàn)的。
  2. 程序使用動(dòng)態(tài)內(nèi)存出于以下三種原因之一
  • 程序不知道自己需要使用多少對(duì)象
  • 程序不知道所需對(duì)象的準(zhǔn)確類型
  • 程序需要在多個(gè)對(duì)象間共享數(shù)據(jù)
class SharedVector{
public:
    SharedVector(initializer_list<string> l):data(make_shared<vector<string>>(l)){}
    shared_ptr<vector<string>> data;


};

當(dāng)我們拷貝SharedVector對(duì)象時(shí),會(huì)使用默認(rèn)版本的拷貝,因此Shared_ptr也被拷貝,從而實(shí)現(xiàn)了資源的對(duì)象間共享。

  1. C++定義了兩個(gè)運(yùn)算符newdelete來分配和釋放動(dòng)態(tài)內(nèi)存。相對(duì)于智能指針,使用這兩個(gè)運(yùn)算符管理內(nèi)存非常容易出錯(cuò)。
  2. 在自由空間分配的內(nèi)存是無名的,因此new無法為其分配的對(duì)象命名,而是返回一個(gè)指向該對(duì)象的指針:
int *pi=new int;

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

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

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

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};

也可以對(duì)動(dòng)態(tài)分配的對(duì)象進(jìn)行值初始化,只需在類型名之后跟一對(duì)空括號(hào)即可。

int *pi=new int();   //值初始化為0

對(duì)于定義了自己的構(gòu)造函數(shù)的類類型來說,要求值初始化沒有意義(對(duì)象都會(huì)通過默認(rèn)構(gòu)造函數(shù)來初始化),但對(duì)于內(nèi)置類型,值初始化意味著對(duì)象有良好定義的值。

  1. 用new分配const對(duì)象是合法的,一個(gè)動(dòng)態(tài)const對(duì)象必須進(jìn)行初始化
const int *pci=new const int(1024);
  1. 一旦一個(gè)程序用光了它所有可用的內(nèi)存,new表達(dá)式就會(huì)失敗。默認(rèn)情況下,它會(huì)拋出一個(gè)類型為bad_alloc的異常,我們可以改變new的方式來阻止拋出異常:
int *pw=new (nothrow) int;   //如果分配失敗,返回一個(gè)空指針

我們稱這種形式的new為定位new(placement new),定位new允許我們向new傳遞額外的參數(shù)。此處我們傳遞了一個(gè)nothrow對(duì)象。它與bad_alloc都定義在頭文件new中。

  1. 為了防止內(nèi)存耗盡,我們通過delete 表達(dá)式來將動(dòng)態(tài)內(nèi)存歸還給系統(tǒng)。delete接受一個(gè)指針,指向我們想要釋放的對(duì)象:
delete p;  //p必須指向一個(gè)動(dòng)態(tài)分配的對(duì)象或是一個(gè)空指針
  1. 釋放一塊并非new的內(nèi)存,或者將相同的指針釋放多次,其行為是未定義的。
  2. 由內(nèi)置指針管理的動(dòng)態(tài)內(nèi)存在被顯式釋放前會(huì)一直存在,如果不注意這一點(diǎn)很容易造成內(nèi)存泄漏
  3. 當(dāng)我們delete一個(gè)指針后,指針值就變?yōu)闊o效了,該指針就變成了所謂的空懸指針(dangling pointer),即指向一塊曾經(jīng)保存數(shù)據(jù)對(duì)象但現(xiàn)在已經(jīng)無效的內(nèi)存的指針。如果我們需要保留指針,可以在delete之后賦予它nullptr的字面量。但實(shí)際上,可能有多個(gè)指針指向相同的內(nèi)存,這僅僅提供了很有限的保護(hù)。
  4. 如前所述,我們?nèi)绻怀跏蓟粋€(gè)智能指針,它就會(huì)被初始化為一個(gè)空指針。我們可以用new返回的指針來初始化智能指針:
shared_ptr<int> p1(new int(42));

接受指針參數(shù)的智能指針構(gòu)造函數(shù)是explicit的,所以必須使用直接初始化的形式。同理,一個(gè)返回shared_ptr的函數(shù)不能返回一個(gè)普通指針。
默認(rèn)情況下,一個(gè)用來初始化智能指針的普通指針必須指向動(dòng)態(tài)內(nèi)存,因?yàn)橹悄苤羔樐J(rèn)使用delete釋放關(guān)聯(lián)的對(duì)象。但是我們可以提供操作替代delete使得智能指針可以綁定到一個(gè)指向其他類型資源的指針上

image

  1. 當(dāng)將一個(gè)shared_ptr綁定到一個(gè)普通指針時(shí),我們就將內(nèi)存的管理責(zé)任交給了這個(gè)shared_ptr。一旦這么做了,就不應(yīng)該再使用內(nèi)置指針來訪問shared_ptr所指向的內(nèi)存了。
  2. 如果使用智能指針,即使程序塊過早結(jié)束,智能指針類也能確保在內(nèi)存不再需要時(shí)將其釋放。而直接管理的內(nèi)存如果在new之后delete之前發(fā)生了異常,則內(nèi)存不會(huì)釋放
  3. 我們還可以利用智能指針來管理不具有良好定義的析構(gòu)函數(shù)的類。
  4. 為了正確使用智能指針,我們必須堅(jiān)持一些基本規(guī)范
  • 不使用相同的內(nèi)置指針值初始化多個(gè)智能指針
  • 不delete get()返回的指針
  • 不使用get()初始化另一個(gè)智能指針
  • 如果你使用get()返回的指針,記住當(dāng)最后一個(gè)對(duì)應(yīng)的智能指針銷毀后,你的指針就變?yōu)闊o效了
  • 如果你使用智能指針管理的資源不是new分配的內(nèi)存,記住傳遞給它一個(gè)刪除器
  1. 一個(gè)unique_ptr ”擁有“它所指向的對(duì)象,某個(gè)時(shí)刻只能有一個(gè)unique_ptr指向一個(gè)給定對(duì)象。當(dāng)它被銷毀時(shí),指向的對(duì)象也被銷毀。
    沒有類似make_shared的函數(shù)返回一個(gè)unique_ptr。當(dāng)我們定義一個(gè)unique_ptr時(shí),需要將其綁定到一個(gè)new返回的指針上,初始化也同樣必須采用直接初始化
unique_ptr<double> p1;
unique_ptr<int> p2(new int(10));

由于一個(gè)unique_ptr獨(dú)占它指向的對(duì)象,因此不支持普通的拷貝賦值操作。

image

  1. 雖然不能拷貝或賦值unique_ptr,但是可以通過releasereset將指針的所有權(quán)轉(zhuǎn)移:
p2.reset(p3.release());
  1. 不能拷貝unique_ptr的規(guī)則有一個(gè)例外:我們可以拷貝或賦值一個(gè)將要被銷毀的unique_ptr。比如,從函數(shù)返回一個(gè)unique_ptr
  2. 與shared_ptr一樣,我們可以重載一個(gè)unique_ptr中默認(rèn)的刪除器,此處不贅述
  3. weak_ptr是一種不控制所指向?qū)ο笊嫫诘闹悄苤羔槪赶蛴梢粋€(gè)shared_ptr管理的對(duì)象。將一個(gè)weak_ptr綁定到一個(gè)shared_ptr不會(huì)改變shared_ptr的引用計(jì)數(shù)。一旦最后一個(gè)指向?qū)ο蟮膕hared_ptr被銷毀,對(duì)象就會(huì)被釋放,即使有weak_ptr指向?qū)ο蟆?br>
    image
  4. 當(dāng)我們創(chuàng)建一個(gè)weak_ptr時(shí),要用一個(gè)shared_ptr來初始化它:
auto p=make_shared<int>(42);
weak_ptr<int> wp(pP;
  1. 由于對(duì)象可能不存在,我們不能使用weak_ptr直接訪問對(duì)象,而必須調(diào)用lock,此函數(shù)檢查weak_ptr指向的對(duì)象是否仍存在。如果存在,則返回一個(gè)shared_ptr
  2. C++和標(biāo)準(zhǔn)庫提供了兩種一次分配一個(gè)對(duì)象數(shù)組的方法。C++定義了另一種new表達(dá)式語法,可以分配并初始化一個(gè)對(duì)象數(shù)組。標(biāo)準(zhǔn)庫中包含一個(gè)名為allocator的類,允許我們將分配和初始化分離。
  3. 大多數(shù)應(yīng)用應(yīng)該使用標(biāo)準(zhǔn)庫容器而不是動(dòng)態(tài)分配的數(shù)組。使用容器更為簡(jiǎn)單,更不容易出現(xiàn)內(nèi)存管理錯(cuò)誤并且可能有更好的性能。
  4. 為了讓new分配一個(gè)對(duì)象數(shù)組,我們要在類型名之后跟一對(duì)方括號(hào),在其中指明要分配的對(duì)象的數(shù)目,new返回指向第一個(gè)對(duì)象的指針
int *p=new int[size];
  1. 雖然我們通常稱new T[]分配的內(nèi)存為“動(dòng)態(tài)數(shù)組”,但實(shí)際上我們只是得到一個(gè)數(shù)組元素類型的指針動(dòng)態(tài)數(shù)組并不是數(shù)組類型,因此前文所述的begin和end函數(shù),以及范圍for語句,通通不適用
  2. 默認(rèn)情況下,new分配的對(duì)象,都是默認(rèn)初始化的。可以對(duì)數(shù)組中的元素進(jìn)行值初始化,方法是在大小之后跟一對(duì)空括號(hào):
int *p=new int[10]();   //10個(gè)值初始化為0的int

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

int *p=new int[2]{1,2};
  1. 可以用任意表達(dá)式來確定要分配的對(duì)象的數(shù)目:
size_t n=get_size();
int *p=new int[n];
  1. 當(dāng)我們用new分配一個(gè)大小為0的數(shù)組,new返回一個(gè)合法的非空指針。但此指針不能解引用。
  2. 為了釋放動(dòng)態(tài)數(shù)組,我們使用一種特殊形式的delete——指針前加上一個(gè)空括號(hào)對(duì)
delete [ ] p;

數(shù)組中的元素按逆序銷毀。如果沒有方括號(hào),行為是未定義的

  1. 標(biāo)準(zhǔn)庫提供了一個(gè)可以管理new分配的數(shù)組的unique_ptr版本,為了用一個(gè)unique_ptr管理動(dòng)態(tài)數(shù)組,我們必須在對(duì)象類型后面跟一對(duì)空方括號(hào)
unique_ptr<int[]> up(new int[10]);
up.release();    //自動(dòng)用delete[]銷毀其指針
image
  1. shared_ptr不直接支持管理動(dòng)態(tài)數(shù)組,必須提供自己定義的刪除器,此處不展開。
  2. new有一些靈活性上的局限,其中一方面表現(xiàn)在它將內(nèi)存分配和對(duì)象構(gòu)造組合在了一起。標(biāo)準(zhǔn)庫allocator類定義在頭文件memory中,它幫助我們將內(nèi)存分配和對(duì)象構(gòu)造分離開來。它提供一種類型感知的內(nèi)存分配方式,它分配的內(nèi)存是原始的,未構(gòu)造的。
    image
allocator<string> alloc;  // 可以分配 string 的 allocator 對(duì)象
auto const p = alloc.allocate(n);  // 分配 n 個(gè)未初始化的 string

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

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

alloc.deallocate(p, n);  // 釋放內(nèi)存
  1. 為了使用allocator返回的內(nèi)存,必須用construct構(gòu)造對(duì)象,使用位構(gòu)造的內(nèi)存,行為未定義。
    使用完,必須對(duì)每個(gè)構(gòu)造的元素調(diào)用destroy來銷毀,destroy接受一個(gè)指針,對(duì)指向的對(duì)象執(zhí)行析構(gòu)函數(shù)。
    銷毀后,可重新使用這部分內(nèi)存保存其他 string, 也可以釋放內(nèi)存還給系統(tǒng)
  2. 拷貝和填充未初始化內(nèi)存的算法


    拷貝和填充未初始化內(nèi)存的算法
vector<int> vi{1, 2, 3};
allocator<int> alloc;
auto p = alloc.allocate(vi.size() * 2);  // 分配比 vi 中元素所占空間大一倍的動(dòng)態(tài)內(nèi)存
auto q = alloc.unintialized_copy(vi.begin(), vi.end(), p); //拷貝vi中元素構(gòu)造從p開始的元素
uninitialized_fill_n(q, vi.size(), 42);  // 將剩余元素初始化為42
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容