C++ Primer 筆記-第12章 動態(tài)內(nèi)存

靜態(tài)內(nèi)存?用來保存局部?
static
?對象、類?static
?數(shù)局成員以及定義在任何函數(shù)之外的變量。棧內(nèi)存?用來保存定義在函數(shù)內(nèi)的非?
static
?對象。分配在?靜態(tài)?或?棧?內(nèi)存中的對象由編譯器自動創(chuàng)建和銷毀。
對于?棧對象,僅在其定義的程序塊運行時才存在。
static
?對象在使用之前分配,在程序結(jié)束時銷毀。程序用?堆(heap)?來存儲?動態(tài)分配(dynamically allocate)?的對象(程序運行時分配的對象)。動態(tài)對象的生存期由程序來控制,當(dāng)動態(tài)對象不再使用時,我們的代碼必須顯式地銷毀它們。
雖然使用動態(tài)內(nèi)存有時是必要的,但是正確地管理動態(tài)內(nèi)存是非常棘手的。

12.1 動態(tài)內(nèi)存與智能指針
new
?在動態(tài)內(nèi)存中為對象分配空間并返回一個指向該對象的指針,我們可以選擇對對象進行初始化。delete
?接受一個動態(tài)對象的指針,銷毀該對象,并釋放與之關(guān)聯(lián)的內(nèi)存。忘記釋放內(nèi)存,會產(chǎn)生內(nèi)存泄漏。
在尚有指針引用內(nèi)存的情況下釋放了內(nèi)存,會產(chǎn)生引用非法內(nèi)存的指針。
shared_ptr
?允許多個指針指向同一個對象。unique_ptr
?則 “獨占” 所指向的對象。weak_ptr
?是一個伴隨類,一種弱引用,指向?shared_ptr
?所管理的對象。
12.1.1?shared_ptr
類
默認初始化的智能指針中保存著一個空指針。
p = q
:p 和 q 都是?shared_ptr
,此操作會遞減 p 指向?qū)ο蟮囊糜嫈?shù),遞增 q 原來指向?qū)ο蟮囊糜嫈?shù)。若 p 的引用計數(shù)變?yōu)?0,則將其管理的原內(nèi)存釋放。p.use_count()
?返回與 p 共享對象的智能指針數(shù)量,可能很慢,主要用于調(diào)試。到底是用一個計數(shù)器還是其他數(shù)據(jù)結(jié)構(gòu)來記錄有多少指針共享對象,完全由標(biāo)準庫的具體實現(xiàn)來決定。關(guān)鍵是智能指針能記錄有多少個?
shared_ptr
?指向相同的對象,并能在恰當(dāng)?shù)臅r候自動釋放對象。shared_ptr
?在無用之后仍然保留的一種可能情況是:將?shared_ptr
?存放在一個容器中,而后不再需要全部元素,而只使用其中一部分。(在此情況下,要記得用?erase
?刪除不再需要的那些元素。)程序使用動態(tài)內(nèi)存出于以下三種原因之一:
(1)程序不知道自己需要使用多少對象
(2)程序不知道所需對象的準確類型
(3)程序需要在多個對象間共享數(shù)據(jù)下面通過一個例子來詳細說明一下程序需要在多個對象間共享數(shù)據(jù)的情況:

// 數(shù)據(jù)不共享的情況:
vector<string> v1; ? ?// 空 vector
{ ? // 新作用域 ? ?
????vector<string> v2 = { "a", "an", "the" }; ? ?
????v1 = v2; ? ?// 從 v2 拷貝元素到 v1 中
} ? // v2 被銷毀,其中的元素也被銷毀 ? ?
????// v1 有三個元素,是原來 v2 中元素的拷貝
// 數(shù)據(jù)共享的情況:
// 假定我們希望定義一個名為 StrBlob 的類,保存一組元素
// 與容器不同,我們希望 StrBlob 對象的不同拷貝之間共享相同的元素
StrBlob b1; ? ?// 空 StrBlob
{ ? // 新作用域
? ?StrBlob b2 = { "a", "an", "the" };
? ?b1 = b2; ? ?// b2 和 b1 共享相同的元素
} ? // b2 被銷毀了,但是 b2 中的元素不能銷毀
? ??// b1 指向最初的由 b2 創(chuàng)建的元素

接下來我們定義?
StrBlob
?類(使用?vector
?來保存元素,并將?vector
?保存在動態(tài)內(nèi)存中):

// StrBlob.h
class StrBlob{
public:
? ?StrBlob();
? ?StrBlob(std::initializer_list<std::string> il);
private:
? ?std::shared_ptr<str::vector<string>> data;
}
// StrBlob.cpp
StrBlob::StrBlob() : data(make_shared<vector<string>>()) {}
StrBlob::StrBlob(initializer_list<string> il) :
? ? ? ? ? ? ? ?data(make_shared<vector<string>>(il)) {}

當(dāng)我們拷貝、賦值或銷毀一個?
StrBlob
?對象時,它的?Shared_ptr
?成員也會被拷貝、賦值或銷毀。拷貝一個?
shared_ptr
?會遞增其引用計數(shù);將一個?shared_ptr
?賦予另一個?shared_ptr
?會遞增賦值號右側(cè)的?shared_ptr
?的引用計數(shù),而遞減左側(cè)?shared_ptr
?的引用計數(shù)。如果一個?
shared_ptr
?的引用計數(shù)變?yōu)?0 ,它所指向的對象會被自動銷毀。因此,對于由?StrBlob
?構(gòu)造函數(shù)分配的?vector
,當(dāng)最后一個指向它的?StrBlob
?對象被銷毀時,它會隨之被自動銷毀。
12.1.2 直接管理內(nèi)存
C++ 定義了兩個運算符來分配和釋放動態(tài)內(nèi)存:運算符?
new
?分配內(nèi)存,delete
?釋放?new
?分配的內(nèi)存。相對于智能指針,使用這兩個運算符管理內(nèi)存非常容易出錯,在學(xué)習(xí)第13章前,除非使用智能指針來管理內(nèi)存,否則不要分配動態(tài)內(nèi)存。
在自由空間(堆)分配的內(nèi)存是?無名?的,因此?
new
?無法為其分配的對象命名,而是返回一個指向該對象的指針:int* pi = new int; ? ? ? ? ?// pi指向一個動態(tài)分配的、未初始化的無名對象
默認情況下,動態(tài)分配的對象是?默認初始化?的,這意味著?內(nèi)置類型?或?組合類型的對象?的值將是?未定義?的,而?類類型對象?將用?默認構(gòu)造函數(shù)?進行初始化。
也可以使用?直接初始化(傳統(tǒng)的構(gòu)造方式(使用圓括號)、列表初始化) 和?值初始化(使用空括號)?來動態(tài)分配對象。

// 默認初始化
string* ps1 = new string; ? ? ? ? ? ?// 默認初始化為空string
int* pi1 = new int; ? ? ? ? ? ? ? ? ?// 默認初始化,*pi1的值未定義
// 傳統(tǒng)構(gòu)造方式
int *pi2 = new int(1024); ? ? ? ? ? ?// pi2指向的對象值為1024
string* ps2 = new string(10, '9'); ? // *ps2為“9999999999”
vector<int>* pv = new vector<int>{0, 1, 2, 3}; ?// vector有4個元素,值依次從0到3
// 值初始化
string* ps3 = new string(); ? ? ? ? ?// 值初始化為空string
int* pi3 = new int(); ? ? ? ? ? ? ? ?// 值初始化為0

出于于變量初始化相同的原因,對動態(tài)分配的對象進行初始化通常是個好主意。
對于一個定義了默認構(gòu)造函數(shù)的類類型,其?
const
?動態(tài)對象可以隱式初始化,而其他類型的對象就必須顯示初始化。

const string* pcs = new const string; ? // 隱式初始化
const int* pci = new const int(1024); ? // 顯示初始化

可以使用?定位 new (placement new)?來阻止動態(tài)對象分配時因內(nèi)存耗盡而拋出的異常。

int* p1 = new int; ? ? ? ? ? ? ?// 如果分配失敗,會拋出 std::bad_alloc
// 向new傳遞一個標(biāo)準庫定義的nothrow對象
int* p2 = new (nothrow) int; ? ?// 如果分配失敗,返回一個空指針

delete
?表達式執(zhí)行兩個動作:銷毀給定的指針指向的對象;釋放對應(yīng)的內(nèi)存。傳遞給?
delete
?的指針必須指向?動態(tài)分配的內(nèi)存,或者?空指針。釋放一塊并非?new
?分配的內(nèi)存,或者將相同的指針值釋放多次,其行為是未定義的。通常情況下,編譯器不能分辨一個指針指向的是靜態(tài)還是動態(tài)分配的對象。編譯器也不能分辨一個指針?biāo)赶虻膬?nèi)存是否以及被釋放了。
由內(nèi)置指針(而不是智能指針)管理的動態(tài)內(nèi)存在被顯示釋放前一直都會存在。
使用?
new
?和?delete
?管理動態(tài)內(nèi)存有三個常見的問題:
(1)忘記?delete
?內(nèi)存;
(2)使用已經(jīng)釋放掉的對象;
(3)同一塊內(nèi)存被釋放兩次。堅持只使用?智能指針,就可以避免所有這些問題。對于一塊內(nèi)存,只有在沒有任何智能指針指向它的情況下,智能指針才會自動釋放它。
在?
delete
?之后,指針就變成了?空懸指針(dangling pointer)(指向一塊曾經(jīng)保存數(shù)據(jù)對象但現(xiàn)在已經(jīng)無效的內(nèi)存的指針),在?delete
?之后將?nullptr
?賦予指針,可以清楚的指出指針不指向任何對象。當(dāng)有多個指針指向相同的內(nèi)存時,在?
delete
?內(nèi)存之后重置指針的辦法只對這個指針有效,對其他任何仍指向(已釋放的)內(nèi)存的指針是沒有用的。

int* p(new int(1024)); ?// p指向動態(tài)內(nèi)存
auto q = p; ? ? ? ? ? ? // p和q指向同一塊內(nèi)存
delete p; ? ? ? ? ? ? ? // p和q均變?yōu)闊o效
p = nullptr; ? ? ? ? ? ?// 此時p不再綁定到任何對象
// 此時重置p對q沒有任何作用,q仍然指向原來那塊(已經(jīng)被釋放的)內(nèi)存地址
// 在實際系統(tǒng)中,查找指向相同內(nèi)存的所有指針是異常困難的!

12.1.3?shared_ptr
?和?new
?結(jié)合使用
接受指針參數(shù)的智能指針構(gòu)造函數(shù)是?
explicit
?的。因此,我們不能將一個內(nèi)置指針隱式轉(zhuǎn)換為一個智能指針,必須使用?直接初始化形式?來初始化一個智能指針。:

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

一個返回?
shared_ptr
?的函數(shù)不能在其返回語句中隱式轉(zhuǎn)換一個普通指針:

shared_ptr<int> clone(int p){
? ?return new int(p); ? ? ? ? ? ? ? ? ????????????// 錯誤
? ?return shared_ptr<int>(new int(p)); // 正確
}

一般情況下,一個用來初始化智能指針的普通指針必須指向動態(tài)內(nèi)存,因此智能指針默認使用?
delete
?釋放它所關(guān)聯(lián)的對象。 但是也可以將智能指針綁定到一個指向其他類型的資源的指針上,只是此時需要提供自己的釋放操作來替代?delete
。

// 如果p是唯一指向其對象的shared_ptr,reset會釋放此對象。
p.reset();
// 若傳遞了可選的參數(shù)內(nèi)置指針q,會令p指向q,否則會將p置為空。
p.reset(q);
//若還傳遞了參數(shù)d,將會調(diào)用d而不是delete來釋放q
p.reset(q, d);

不要混合使用普通指針和智能指針,使用一個內(nèi)置指針來訪問一個智能指針?biāo)撠?zé)的對象是很危險的,因為我們無法知道對象何時會被銷毀:

// ptr是傳值方式傳遞,因此拷貝時會遞增其引用次數(shù)。
void process(shared_ptr<int> ptr){
? ? // 使用ptr
} ? // ptr離開作用域,ptr會被銷毀。由于ptr銷毀了,引用次數(shù)會遞減
// process正確的使用方法:
shared_ptr<int> p(new int(42)); // 此時引用計數(shù)為1
process(p); ? ? // 拷貝p會遞增其引用次數(shù),此時在process中引用計數(shù)值為2
int i = *p;? ? ? ? ?// 離開了process作用域,引用計數(shù)值為1?
// process錯誤的使用方法:
int *x(new int(1024)); ?// 危險:x是普通指針,不是智能指針
process(x);? ? ? ? ? ? ? ?? // 錯誤:不能將int* 轉(zhuǎn)換為shared_ptr<int>
process(shared_ptr<int>(x)); // 合法,但是由于傳參時使用的是一個臨時變量,
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? // 臨時變量在傳遞完之后會被銷毀,所以傳完參
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??// 之后的process里的ptr引用計數(shù)為1
int j = *x; // 未定義:離開了process作用域,引用計數(shù)值為0,此時x為一個懸空指針

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

shared_ptr<int> p(new int(42)); // 引用計數(shù)為1
// 新程序塊
{
? ?// 兩個獨立的shared_ptr指向相同的內(nèi)存
? ? ? ?shared_ptr<int> q(p.get()); ? // 錯誤:將get用來初始化指針指針
} ? // 程序塊結(jié)束,q被銷毀,它指向的內(nèi)存被釋放
int foo = *p; ? // 未定義:p指向的內(nèi)存已經(jīng)被釋放了

12.1.4 智能指針和異常
那些分配了資源,而又?沒有定義析構(gòu)函數(shù)?來釋放這些資源的類,可能會遇到與使用動態(tài)內(nèi)存相同的錯誤——程序員非常容易忘記釋放資源。與管理動態(tài)內(nèi)存類似,我們通??梢允褂弥悄苤羔榿砉芾聿痪哂辛己枚x的析構(gòu)函數(shù)的類:

class destination;
class connection; ? // 假定connection沒有析構(gòu)函數(shù)
connection connect(destination* d) {}
void disconnect(connection c) {}
void f(destination& d){
? ?connection c = connect(&d);
? ?// ...
? ?// 如果我們在f退出前忘記調(diào)用disconnect,就無法關(guān)閉c了
}
// 使用智能指針管理對象的生命周期
// 定義刪除器
void end_connection(connection* p) { disconnect(*p); }
void f(destination& d){?
? ?connection c = connect(&d);
? ?shared_ptr<connection> p(&c, end_connection);
? ?// ...
? ?// 當(dāng)f退出時(即便是由于異常而退出),connection會被正確關(guān)閉
}

智能指針陷阱:
(1)不使用相同的內(nèi)置指針值初始化(或?
reset
?)多個智能指針。
(2)不?delete
?get()
?返回的指針。
(3)不使用?get()
?初始化或?reset
?另一個智能指針。
(4)如果你使用?get()
?返回的指針,記住當(dāng)最后一個對應(yīng)的智能指針銷毀后,你的指針就變?yōu)闊o效了。
(5)如果你使用智能指針管理的資源不是?new
?分配的內(nèi)存,記住傳遞給它一個刪除器。
12.1.5?unique_ptr
一個?
unique_ptr
?“擁有” 它所指向的對象。某個時刻只能有一個?unique_ptr
?指向一個給定對象,當(dāng)?unique_ptr
?被銷毀時,它所指向的對象也被銷毀。定義一個?
unique_ptr
?時,需要將其綁定到一個?new
?返回的指針上。初始化?unique_ptr
?必須采用直接初始化形式。unique_ptr
?不支持普通的拷貝或賦值操作。不能拷貝?
unique_ptr
?的規(guī)則有一個例外:我們可以拷貝或賦值一個將要被銷毀的?unique_ptr
。常見的例子是從函數(shù)返回一個?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
?傳遞刪除器:

// 將之前的例子中的shared_ptr換成unique_ptr
void f(destination& d){
? ?connection c = connect(&d);
? ?// 使用decltype來指明函數(shù)指針類型
? ?// 必須加一個*來指出我們正在使用該類型的指針
? ?unique_ptr<connection, decltype(end_connection)*>
? ? ? ?p(&c, end_connection);
}

12.1.6?weak_ptr
weak_ptr
是一種不控制所指向?qū)ο笊嫫诘闹悄苤羔?,它指向由一個?shared_ptr
?管理的對象。將一個?
weak_ptr
?綁定到一個?shared_ptr
?不會改變?shared_ptr
?的引用計數(shù)。一旦最后一個指向?qū)ο蟮?shared_ptr
?被銷毀,對象就會被釋放。即使有?weak_ptr
?指向?qū)ο?,對象也還是會被釋放。因此,weak_ptr
?的名字抓住了這種智能指針 “弱” 共享對象的特點。由于對象可能不存在,我們不能使用?
weak_ptr
?直接訪問對象,而必須調(diào)用?lock
。檢查對象是否存在,如果存在,lock
?返回一個指向共享對象的?shared_ptr
,否則返回一個空?shared_ptr
:

auto p = make_shared<int>(42);
weak_ptr<int> wp(p); ? ?// wp弱共享p;p的引用計數(shù)未改變
// 如果np不為空則條件成立
if(shared_ptr<int> np = wp.lock()) {
? ?// 在if中,np與p共享對象
}

12.2 動態(tài)數(shù)組
大多數(shù)應(yīng)用應(yīng)該使用?標(biāo)準庫容器?而不是?動態(tài)分配的數(shù)組。使用容器更為簡單、更不容易出現(xiàn)內(nèi)存管理錯誤并且可能有更好的性能。
使用容器的類可以使用默認版本的拷貝、賦值和析構(gòu)操作。分配動態(tài)數(shù)組的類則必須定義自己版本的操作,在拷貝、賦值以及銷毀對象時管理所關(guān)聯(lián)的內(nèi)存。
12.2.1?new
?和數(shù)組
為了讓?
new
?分配一個對象數(shù)組,我們要在類型名之后跟一對方括號,指明要分配的對象的數(shù)目。

int* p1 = new int[42]; ?// p1指向第一個int
//也可以使用一個表達數(shù)組類型的類型別名來分配一個數(shù)組
typedef int arrT[42]; ? // arrT表示42個int的數(shù)組類型
int* p = new arrT; ? ? ?// 分配一個42個int的數(shù)組,p指向第一個int

通常稱?
new T[]
?分配的內(nèi)存為 “動態(tài)數(shù)組”,但是這種叫法有些誤導(dǎo)。實際上我們得到的是一個指向第一個元素的數(shù)組元素類型的指針。不能對動態(tài)數(shù)組調(diào)用?
begin
?、end
?和范圍?for
?循環(huán)操作。可以對數(shù)組中的元素進行值初始化,方法是加()或{}列表。

int* pia = new int[3]; ? ? ? ? ? ? ?// 3個未初始化的int
int* pia2 = new int[3](); ? ? ? ? ? // 3個值初始化為0的int
int* pia3 = new int[3]{0, 1, 2}; ? ?// 跟列表相同的int
int* pia4 = new int[3]{0, 1, 2, 3}; // 列表數(shù)目超出,拋出bad_array_new_length異常
// 雖然可以用空括號對數(shù)組中元素的進行值初始化
// 但是不能在括號中給出初始化器
// 這意味著不能用auto分配數(shù)組
int obj = 1;auto p = new auto[3](obj);

int* p = new int[0];
:當(dāng)用?new
?分配一個大小為0的數(shù)組時,new
?返回一個合法的非空指針。此指針保證與?new
?返回的其他任何指針都不相同。我們可以像使用尾后迭代器一樣使此指針。但是此指針?不能解引用。delete [] p;
?釋放一個指向數(shù)組的指針時,方括號是必須的。在?
delete
?一個數(shù)組時忘記了方括號,或者在?delete
?一個單一對象的指針時使用了方括號,編譯器很可能?不會給出警告。我們的程序可能在執(zhí)行過程中在沒有任何警告的情況下行為異常(行為是未定義的)。標(biāo)準庫提供一個可以管理?
new
?分配到數(shù)組的?unique_ptr
?版本,但是不能使用點和箭頭成員運算符操縱。

unique_ptr<int[]> up(new int[10]);
for(size_t i = 0; i != 10; ++i)
? ?up[i] = i; ?// 可以使用下標(biāo)運算符來訪問數(shù)組中的元素。
up.release(); ? // 自動用delete[]銷毀其指針

shared_ptr
?不直接支持管理動態(tài)數(shù)組。如果希望使用,必須提供自己定義的刪除器。否則將會使用默認的?delete
?而不是?delete[]
?銷毀對象。

shared_ptr<int> sp(new int[10, [](int* p) { delete[] p; }]);
// shared_ptr 未定義下標(biāo)運算符,并且不支持指針的算數(shù)運算
for(size_t i = 0; i != 10; ++i)
? ?????*(sp.get() + i) = i; ? ?// 必須用get獲取內(nèi)置指針才能訪問元素
sp.reset(); // shared_ptr沒有release()

12.2.2?allocator
?類
當(dāng)分配單個對象時,通常希望將內(nèi)存分配和對象初始化組合在一起(對應(yīng)操作是?
new
?和?delete
)。當(dāng)分配一大塊內(nèi)存時,我們通常計劃在一塊內(nèi)存上按需構(gòu)造對象,此時我們希望內(nèi)存分配和對象構(gòu)造分離標(biāo)準庫?
allocator
?類幫助我們將內(nèi)存分配和對象構(gòu)造分離開來。它提供一種類型感知的內(nèi)存分配方式,它分配的內(nèi)存是原始的、未構(gòu)造的。

int n = 3;
allocator<string> alloc; ? ? ? // 可以分配string的allocator對象
auto const p = alloc.allocate(n);// 分配n個未初始化的string
auto q = p; ? ? ? ? ? ? ? ? ? ?// q指向最后構(gòu)造的元素之后的位置
alloc.construct(q++); ? ? ? ? ?// *q為空字符串
alloc.construct(q++, 10, 'c'); // *q為cccccccccc
alloc.construct(q++, "hi"); ? ?// *q為hi!
// 在還未構(gòu)造對象的情況下使用原視內(nèi)存是錯誤的:
cout << *p << endl; ? ? ? ? ? ?// 正確:使用string的輸出運算符
cout << *q << endl; ? ? ? ? ? ?// 錯誤:q指向未構(gòu)造的內(nèi)存!
// 用完對象后,必須銷毀每個構(gòu)造的元素
while(q != p)
? ?alloc.destroy(--q); ? ? ? ?// 釋放我們真正構(gòu)造的string
// 釋放內(nèi)存
alloc.deallocate(p, n);

標(biāo)準庫還為?
allocator
?類定義了兩個伴隨算法,可以在未初始化內(nèi)存中創(chuàng)建對象:

vector<int> vi{0, 1, 2, 3};
// 分配一塊比vector中元素所占用空間大一倍的動態(tài)內(nèi)存,
allocator<string> alloc;
auto p = alloc.allocate(vi.size() * 2);
// 然后將原vector中的元素拷貝到前一半空間,
// copy有兩種寫法:
// q 指向最后一個構(gòu)造的元素之后的位置。
auto q = uninitialized_copy(vi.begin(), vi.end(), p);
auto q = uninitialized_copy_n(vi.begin(), vi.size(), p);
// 對后一半的空間用一個給定值進行填充,
// fill也有兩種寫法:
uninitialized_fill(q, q + n, 42);uninitialized_fill_n(q, vi.size(), 42);


