1. 程式人生 > 其它 >動態記憶體與智慧指標

動態記憶體與智慧指標

一、介紹

全域性物件在程式啟動時分配,在程式結束時銷燬。

對於區域性自動物件,當我們進入其定義所在的程式塊時被建立,在離開塊時銷燬。

區域性static物件在第一次使用前分配,在程式結束時銷燬。

動態分配的物件的生存期與它們在哪裡建立是無關的,只有當顯式地被釋放時,這些物件才會銷燬。

靜態記憶體用來儲存區域性static物件、類static資料成員以及定義在任何函式之外的變數。

棧記憶體用來儲存定義在函式內的非static物件,分配在靜態或棧記憶體中的物件由編譯器自動建立和銷燬。對於棧物件,僅在其定義的程式塊執行時才存在;static物件在使用之前分配,在程式結束時銷燬。

除了靜態記憶體和棧記憶體,每個程式還擁有一個記憶體池

。這部分記憶體被稱作自由空間。程式用堆來儲存動態分配的物件——即,那些在程式執行時分配的物件。動態物件的生存期由程式來控制, 也就是說,當動態物件不再使用時,我們的程式碼必須顯式地銷燬它們。

二、使用

動態記憶體的關聯:newdelete

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_ptrunique_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類會自動釋放動態物件。這一特性使得動態記憶體的使用變得非常容易。

程式使用動態記憶體出於以下三種原因:

  1. 程式不知道自己需要使用多少物件
  2. 程式不知道所需物件的準確型別
  3. 程式需要在多個物件間共享資料

容器類是出於第一種原因而使用動態記憶體的典型例子。

四、直接管理記憶體

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_allocnothrow都定義在標頭檔案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);  // 呼叫者負責釋放此記憶體
}

與類型別不同,內建型別的物件被銷燬時什麼也不會發生。特別是,當一個指標離開其作用域時,它所指向的物件什麼也不會發生。如果這個指標指向的是動態記憶體,那麼記憶體將不會被自動釋放。

由內建指標(而不是智慧指標)管理的動態記憶體在被顯式釋放前一直都會存在。