動態記憶體與智慧指標
一、介紹
全域性物件在程式啟動時分配,在程式結束時銷燬。
對於區域性自動物件,當我們進入其定義所在的程式塊時被建立,在離開塊時銷燬。
區域性static
物件在第一次使用前分配,在程式結束時銷燬。
動態分配的物件的生存期與它們在哪裡建立是無關的,只有當顯式地被釋放時,這些物件才會銷燬。
靜態記憶體
用來儲存區域性static
物件、類static
資料成員以及定義在任何函式之外的變數。
棧記憶體
用來儲存定義在函式內的非static
物件,分配在靜態或棧記憶體中的物件由編譯器自動建立和銷燬。對於棧物件,僅在其定義的程式塊執行時才存在;static
物件在使用之前分配,在程式結束時銷燬。
除了靜態記憶體和棧記憶體,每個程式還擁有一個記憶體池
自由空間
或堆
。程式用堆來儲存動態分配
的物件——即,那些在程式執行時分配的物件。動態物件的生存期由程式來控制, 也就是說,當動態物件不再使用時,我們的程式碼必須顯式地銷燬它們。
二、使用
動態記憶體的關聯:new
和delete
new
:在動態記憶體中為物件分配空間並返回一個指向該物件的指標。
delete
:接受一個動態物件的指標,銷燬該物件,並釋放與之關聯的記憶體。
智慧指標
負責自動釋放所指向的物件。
shared_ptr
允許多個指標指向同一個物件;
unique_ptr
則獨佔所指向的物件。
weak_ptr
是弱引用,指向shared_ptr
所管理的物件。
這三種類型都定義在memory
三、shared_ptr
類
shared_ptr<string> p1; // shared_ptr, 可以指向string
shared_ptr<list<int>> p2; // shared_ptr, 可以指向int的list
預設初始化的智慧指標中儲存著一個空指標。
解引用一個智慧指標返回它所指向的物件。如果在一個條件判斷中使用智慧指標,效果就是檢測它是否為空:
// 如果p1不為空,檢查它是否指向一個空string if (p1 && p1->empty()) { *p1 = "hi"; // 如果p1指向一個空string, 解引用p1, 將一個新值賦予string }
shared_ptr 和unique_ptr 都支援的操作 |
說明 |
---|---|
shared_ptr<T> sp |
空智慧指標,可以指向型別為T的物件 |
unique_ptr<T> up |
同上 |
p | 將p用作一個條件判斷, 若p指向一個物件, 則為true |
*p | 解引用p, 獲得它所指向的物件 |
p->mem | 等價於(*p).mem |
p.get() | 返回p中儲存的指標。要小心使用,若智慧指標釋放了其物件,返回的指標所指向的物件也就消失了 |
swap(p, q) | 交換p和q中的指標 |
p.swap(q) | 同上 |
shared_ptr 獨有的操作 |
說明 |
---|---|
make_shared<T> (args) |
返回一個shared_ptr,指向IG動態分配的型別為T的物件。使用args初始化此物件 |
shared_ptr<T>p(q) |
p是shared_ptr q的拷貝:此操作會遞增q中的計數器。q中的指標必須能轉換為 T* |
p = q | p和q都是shared_ptr,所儲存的指標必須能相互轉換。次操作會遞減p的引用計數,遞增q的引用計數;若q的引用計數變為0, 則將其管理的記憶體釋放 |
p.unique() | 若 p.use_count() 為1,返回true;否則返回false |
p.use_count() | 返回與p共享物件的智慧指標數量:可能很慢,主要用於除錯 |
make_shared
函式
最安全的分配和使用動態記憶體的方法是呼叫一個名為make_shared
的標準庫函式。
此函式在動態記憶體中分配一個物件並初始化它,返回指向此物件的shared_ptr
。與智慧指標一樣,make_shared
也定義在標頭檔案memory
中。
// 指向一個值為42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4指向一個值為 "99999999"的string
shared_ptr<string> p4 = make_shared<string>(10, '9');
// p5指向一個值初始化的int,即,值為0
shared_ptr<int> p5 = make_shared<int>();
通常使用auto
定義一個物件來儲存make_shared
的結果,這種方式較為簡單:
// p6指向一個動態分配的空vector<string>
auto p6 = make_shared<vector<string>>();
shared_ptr
的拷貝和賦值
auto p = make_shared<int>(42); // p指向的物件只有p一個引用者
auto q(p); // p和q指向相同物件,此物件有兩個引用者
auto r = make_shared<int>(42); // r指向的int只有一個引用者
r = q; // 給r賦值,令它指向另一個地址
// 遞增q指向的物件的引用計數
// 遞減r原來指向的物件的引用計數
// r原來指向的物件已沒有引用者, 會自動釋放
shared_pt
自動銷燬
當指向一個物件的最後一個shared_ptr
被銷燬時,shared_ptr
類會自動銷燬此物件。
它是通過另一個特殊的成員函式————解構函式完成銷燬工作的。類似於建構函式,每個類都有一個解構函式
。
解構函式
一般用來釋放物件所分配的資源。
shared_ptr
的解構函式會遞減它所指向的物件的引用計數。如果引用計數變為0,shared_ptr
的解構函式就會銷燬物件,並釋放它所佔用的記憶體。
當動態物件不再被使用時,shared_ptr
類會自動釋放動態物件。這一特性使得動態記憶體的使用變得非常容易。
程式使用動態記憶體出於以下三種原因:
- 程式不知道自己需要使用多少物件
- 程式不知道所需物件的準確型別
- 程式需要在多個物件間共享資料
容器類是出於第一種原因而使用動態記憶體的典型例子。
四、直接管理記憶體
C++語言定義了兩個運算子來分配和釋放動態記憶體。運算子new
分配記憶體,delete
釋放new
分配的記憶體。
在自由空間分配的記憶體是無名的,因此new
無法為其分配的物件命名,而是返回一個指向該物件的指標:
int *pi = new int; // pi指向一個動態分配的、未初始化的無名物件
string *ps = new string; // 初始化為空string
int *pi = new int; // pi指向一個未初始化的int
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};
如果我們提供了一個括號包圍的初始化器,就可以使用auto
從此初始化器來推斷我們想要分配的物件的型別。
auto p1 = new auto(obj); // p指向一個與obj型別相同的物件
// 該物件用obj進行初始化
auto p2 = new auto{a, b, c}; // 錯誤:括號中只能有單個初始化器
記憶體耗盡
一旦一個程式用光了它所有可用的記憶體,new
表示式就會失敗。預設情況下,如果new
不能分配所要求的記憶體空間,它會丟擲一個型別為bad_alloc
的異常。我們可以改變使用new
的方式來阻止它丟擲異常:
// 如果分配失敗,new返回一個空指標
int *p1 = new int; // 如果分配失敗,new丟擲std::bad_alloc
int *p2 = new (nothrow) int; // 如果分配失敗, new返回一個空指標
我們稱這種形式的new
為定位new
。定位new
表示式允許我們向new
傳遞額外的引數。在此例中,我們傳遞給它一個由標準庫定義的名為nothrow
的物件。如果將nothrow
傳遞給new
,我們的意圖是告訴它不能丟擲異常。如果這種形式的new
不能分配所需記憶體,它會返回一個空指標。bad_alloc
和nothrow
都定義在標頭檔案new
中。
釋放動態記憶體
為了防止記憶體耗盡,在動態記憶體使用完畢後,必須將其歸還給系統。我們通過delete
表示式來將動態記憶體歸還給系統。delete
表示式接受一個指標,指向我們想要釋放的物件:
delete p; // p必須指向一個動態分配的物件或是一個空指標
與new
型別類似,delete
表示式也執行兩個動作:銷燬給定的指標指向的物件:釋放對應的記憶體。
指標值和delete
我們傳遞給delete
的指標必須指向動態分配的記憶體,或者是一個空指標。釋放一塊並非new
分配的記憶體,或者將相同的指標值釋放多次,其行為是未定義的:
int i, *pil = &i, *pi2 = nullptr;
double *pd = new double(33), *pd2 = pd;
delete i; // 錯誤: i不是一個指標
delete pil; // 未定義:pil 指向一個區域性變數
delete pd; // 正確
delete pd2; // 未定義:pd2指向的記憶體已經被釋放了
delete pi2; // 正確:釋放一個空指標總是沒有錯誤的
雖然一個const
物件的值不能被改變,但它本身是可以被銷燬的。如同任何其他動態物件一樣,想要釋放一個const
動態物件,只要delete
指向它的指標即可:
const int *pci = new const int(1024);
delete pci; // 正確:釋放一個const物件
動態物件的生存期直到被釋放時為止
由shared_ptr
管理的記憶體在最後一個shared_ptr
銷燬時會被自動釋放。但對於通過內建指標型別來管理的記憶體,就不是這樣了。對於一個由內建指標管理的動態物件,直到被顯式釋放之前它都是存在的。
返回指向動態記憶體的指標(而不是智慧指標)的函式給其呼叫者增加了一個額外負擔——呼叫者必須記得釋放記憶體:
// factory返回一個指標,指向一個動態分配的物件
Foo* factory(T arg) {
// 視情況處理arg
return new Foo(arg); // 呼叫者負責釋放此記憶體
}
與類型別不同,內建型別的物件被銷燬時什麼也不會發生。特別是,當一個指標離開其作用域時,它所指向的物件什麼也不會發生。如果這個指標指向的是動態記憶體,那麼記憶體將不會被自動釋放。
由內建指標(而不是智慧指標)管理的動態記憶體在被顯式釋放前一直都會存在。