C++ 學習筆記之(12) - 動態內存、智能指針和動態數組
程序中所使用的對象都有嚴格定義的生存期。
- 全局對象:在程序啓動時分配,程序結束時銷燬
- 局部自動對象:程序進入其定義所在塊時創建,離開塊時銷燬
- 局部
static
對象:第一次使用前分配,程序結束時銷燬 - 動態分配對象:顯示創建,顯示釋放
內存存放區間
- 靜態內存:局部
static
對象、類static
數據成員以及定義在任何函數之外的而變量 - 棧內存:定義在函數內的非
static
對象 - 自由空間(堆):動態分配對象,動態分配對象的生存期由程序控制
動態內存與智能指針
動態內存的管理是通過一對運算符完成的
- new:在動態內存中爲對象分配空間並返回一個指向該對象的指針,用來對對象進行初始化
- delete:接受一個動態對象的指針,銷燬該對象,並釋放與之關聯的內存
動態內存的使用容器出問題
- 內存泄漏:忘記釋放內存
- 非法內存指針:在尚有指針引用內存的情況下就釋放了呢村
新標準庫提供了兩種智能指針類型管理動態對象,智能指針與常規指針的區別在於它負責自動釋放所指向的對象
shared_ptr
:允許多個指針指向同一個對象unique_ptr
:獨佔所指向的對象weak_ptr
:一種弱引用,指向shared_ptr
所管理的對象
shared_ptr
類
創建智能指針
shared_ptr<string> p1; // shared_ptr, 可以指向 string
shared_ptr<list<int>> p2; // shared_ptr, 可以指向 int 的 list
make_shared
函數
make_shared
在動態內存中分配一個隊形並初始化它,返回指向此對象的shared_ptr
, 用其參數來構造給定類型的對象
// 指向一個值爲“9999999999” 的 string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// 也可使用 auto, p6 指向一個動態分配的空 vector<string>
auto p6 = make_shared<vector<string>>();
引用計數
- 拷貝時計數器遞增
- 一個
shared_ptr
初始化另一個shared_ptr
- 將
shared_ptr
作爲參數傳遞給函數 - 將
shared_ptr
作爲函數的返回值
- 一個
- 計數器遞減
- 給
shared_ptr
賦予新值時 shared_ptr
銷燬時(比如局部shared_ptr
對象離開其作用域)
- 給
auto r = make_shared<int>(42); // r 指向的 int 只有一個引用者
r = q; // 給 r 賦值,令它指向另一個地址
// 遞增 q 指向的對象的引用計數
// 遞減 r 原來指向的對象的引用計數
// r 原來指向的對象已沒有引用者,會自動釋放
shared_ptr
銷燬和釋放內存
shared_ptr
的析構函數會遞減它所指向的對象的引用計數,如果引用技術變爲0
,shared_ptr
的析構函數就會銷燬對象,並釋放其佔用的內存
void function(T arg)
{
shared_ptr<int> p = make_shared<int>(125);
// 情況1:不返回, p 爲局部變量,函數結束時被銷燬,p被銷燬時,引用計數遞減爲0,故內存被釋放
return p; // 情況2:返回 p 的拷貝, 引用計數進行遞增操作,故即使 p 被銷燬,但內存還有其他使用者,不會被釋放
}
使用動態內存的原因
- 程序不知道自己需要使用多少對象, 比如容器類
- 程序不知道所需對象的準確類型
- 程序需要在多個對象間共享數據
直接管理內存
C++語言定義了兩個運算符來分配和釋放動態內存,相對於智能指針,直接管理容器出錯
new
:分配內存,並返回一個指向該對象的指針。// 默認情況下,動態分配對象是默認初始化的,即內置類型或組合類型的對象的值將未定義,類使用默認構造函數 int *pi = new int; // pi 指向一個動態分配的、未初始化的無名對象,即默認初始化,`*pi值未定義 // 也可對動態分配對象進行值初始化,只需在類型名之後跟一對控括號 int *pi2 = new int(); // 值初始化爲0, *pi2 爲0 ///若括號中僅有單一初始化器,可使用 auto 推斷想要分配的對象類型 auto p1 = new auto(obj); // p1類型是指針,指向從 obj 自動推斷出的類型 auto p2 new auto{a, b, c}; // 錯誤:括號中只能有單個初始化器 //用 new 分配 const 對象是合法的, 下爲分配並初始化一個 const int const int *pci = new const int(1024);
delete
:銷燬給定的指針指向的對象,釋放對應的內存。必須是指向動態分配的指針,或空指針。釋放一塊並非new
分配的內存,或者將相同的指針釋放多次,其行爲是未定義的空懸置鎮:指向一塊曾經保存數據對象但現在已經無效的內存的指針
shared_ptr
和new
結合使用
接受指針參數的智能指針構造函數是
explicit
的,故不能講一個內置指針隱式轉換爲智能指針,必須使用直接初始化方式shared_ptr<int> p1 = new int(1024); // 錯誤:必須使用直接初始化方式 shared_ptr<int> p2(new int(1024)); // 正確:使用了直接初始化方式
不要混合使用普通指針和智能指針,因爲內置指針訪問智能指針所負責的對象是很危險的,因爲無法知道對象何時會被銷燬
不要使用
get
初始化另一個智能指針或爲智能指針賦值。get
定義在智能指針類型中,迎來返回一個內置指針,指向智能指針管理的對象。shared_ptr<int> p(new int(42)); // 引用計數爲 1 int *q = p.get(); // 正確:但使用 q 時要注意,不要讓他管理的指針被釋放 {// 新程序塊 // 未定義:兩個獨立的`shared_ptr`指向相同的內存 shared_ptr<int>(q) }// 程序塊結束,q 被銷燬,它指向的內存被釋放 int foot = *p; // 未定義:p 指向的內存已經被釋放了
智能指針和異常
在發生異常時,智能指針仍能正確釋放,但直接管理的內存不會自動釋放
void f() { shared_ptr<int> sp(new int(42)); // 分配一個新對象 int *ip = new int(42); // 動態分配一個新對象 // 這段代碼拋出一個異常,且在 f 中未被捕獲 delete ip; // 在推出之前釋放內存,發生異常時,沒有執行,被跳過 } // 在函數結束時 shared_ptr 自動釋放內存
刪除器:釋放
shared_ptr
中保存的指針,用來取代shared_ptr
被銷燬時默認進行的delete
操作,在創建shared_ptr
時即可指定正確使用智能指針的接本規範
- 不使用相同的內置指針值初始化(或
reset
)多個智能指針 - 不
delete``get()
返回的指針 - 不使用
get()
初始化或reset
另一個智能指針 - 如果你使用
get()
返回的指針,記住當最後一個對應的智能指針銷燬後,它會失效 - 如果智能指針管理的資源表示
new
分配的內存,記住傳遞給他一個刪除器
- 不使用相同的內置指針值初始化(或
unique_ptr
unique_ptr
擁有它所指向的對象,即某時刻只能有一個unique_ptr
指向某對象,當其被銷燬時,所指向的對象也被銷燬
unique_ptr
無類似make_shared
函數。定義時,需要將其綁定到new
返回的指針上類似
shared_ptr
,初始化unique_ptr
需採用直接初始化形式unique_ptr
獨佔其指向的對象,故不支持普通的拷貝或賦值操作unique_ptr<string> p1(new string("hao")); // p2 指向一個值爲 hao 的 string unique_ptr<string> p2(p1); // 錯誤:unique_ptr 不支持拷貝 unique_ptr<string> p3; p3 = p2; // 錯誤:unique_ptr 不支持賦值
unique_ptr
雖然不能賦值或拷貝,但可以調用release
或reset
將所有權從非const
的unique_ptr
轉移到另一個unique_ptr
// 接上 unique_ptr<string> p2(p1.release()); // release 將 p1 置爲空,並將所有權轉移給 p2
unique_ptr
在將被銷燬時可以拷貝或賦值, 比如從函數返回unique_ptr
unique_ptr<int> clone(int p) { return unique_ptr<int>(new int(p)); }
類似
shared_ptr
,unique_ptr
也可重載其默認的刪除器// p 指向一個類型爲 objT 的對象,並使用一個類型爲 delT 的對象釋放 objT對象 // 它會調用一個名爲 fcn 的 delT 類型對象 unique_ptr<objT, delT> p (new objT, fcn);
weak_ptr
一種不控制所指向對象生存期的智能指針,指向由一個shared_ptr
管理的對象。且綁定後不會改變shared_ptr
的引用計數
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp 弱共享 p; p 的引用計數未改變
// 由於對象可能不存在,故不能使用 weak_ptr 直接訪問隊形,而必須調用 lock
if(shared_ptr<int> np = wp.lock()) // 如果 np 不爲空則條件成立
{
// 在 if 中, np 與 p 共享對象
}
動態數組
C++語言和標準庫提供了兩種一次分配一個對象數組的方法。
- C++ 語言定義了另一種
new
表達式語法,可以分配並初始化一個對象數組 - 標準庫提供了
allocator
類,允許分配和初始化分離,性能更好且更靈活
new
和數組
new
分配數組int *pia = new int[5]; // pia 指向第一個 int typedef int arrT[42]; // arrT 表示 42 個 int 的數組類型 int *p = new arrT; // 分配一個 42 個int 的數組; p 指向第一個 int
雖然通常稱
new T[]
分配的內存爲動態數組,但並未得到數組類型對象,而是得到一個數組元素類型的指針,即動態數組並不是數組類型,不能使用某些函數比如begin
、end
或範圍for
語句默認,
new
分配的對象,都執行默認初始化。也可以執行值初始化,只要在後面加()
即可int *pia = new int[10]; // 10 個未初始化的 int int *pia2 = new int[10](); // 10 個值初始化爲 0 的 int int *pia3 = new int[10]{0, 1, 2, 3, 4, ,5}; // 列表初始化,剩餘元素執行值初始化
不能在括號中給出初始化器,即不能用
auto
分配數組釋放動態數組時要在指針前加一個空方括號,即使使用類型別名定義數組,元素逆序銷燬
typedef int arrT[42]; // arrT 表示 42 個 int 的數組類型 int *p = new arrT; // 分配一個 42 個int 的數組; p 指向第一個 int delete [] p; // 方括號必需,因爲分配的是一個數組
標準庫提供了可管理
new
分配的數組的unique_ptr
版本unique_ptr<int[]> up(new int[10]); // up 指向一個包含10個未初始化 int 的數組 up.release(); // 自動用 delete[] 銷燬其指針
若希望使用
shared_ptr
管理動態數組,必須提供自定義的刪除器shared_ptr<int> sp(new int[10], [](int *p){delete[] p;}); // 提供自定義刪除器 sp.reset(); // 使用自定義的 lambda 釋放數組,它使用 delete[]
allocator
類
allocator
類將內存分配和對象構造分離,其分配的內存是原始的、未構造的。
allocator<string> alloc; // 可以分配 string 的 allocator 對象
auto const p = alloc.allocate(n); // 分配 n 個未初始化的 string
auto q = p; // q 指向最後構造的元素之後的位置
alloc.construct(q++); // *q 爲空字符串
alloc.construct(q++, 10, 'c'); // *q 爲 cccccccccc
alloc.construct(q++, "hi"); // *q 爲 hi
while(q != p)
alloc.destroy(--q); // 釋放我們真正構造的 string
alloc.deallocate(p, n); // 釋放內存
爲了使用
allocator
返回的內存,必須用construct
構造對象,使用位構造的內存,行爲未定義使用完,必須對每個構造的元素調用
destroy
來銷燬,destroy
接受一個指針,對指向的對象執行析構函數銷燬後,可重新使用這部分內存保存其他
string
, 也可以釋放內存還給系統拷貝和填充未初始化內存的算法
vector<int> vi{1, 2, 3}; allocator<int> alloc; auto p = alloc.allocate(vi.size() * 2); // 分配比 vi 中元素所佔空間大一倍的動態內存 auto q = alloc.unintialized_copy(vi.begin(), vi.end(), p); //拷貝vi中元素構造從p開始的元素 uninitialized_fill_n(q, vi.size(), 42); // 將剩餘元素初始化爲42
結語
在C++中,內存通過new 表達式分配,通過
delete表達式釋放。標準庫還定義了一個``allocator
類來分配動態內存塊
現在C++程序應儘可能使用智能指針,因爲直接管理很容易出錯