動態內存的使用很容易出問題,因為確保在正確的時間釋放內存是極為困難的。有時我們會忘記釋放內存產生內存泄漏,有時提前釋放了內存,再使用指針去引用內存就會報錯。
為了更容易(同時也更安全)地使用動態內存,新的標准庫提供了兩種智能指針類型來管理動態對象。智能指針的行為類似常規指針,區別在於它負責自動釋放所指向的對象。這兩種智能指針的區別在於管理底層指針的方式:shared_ptr 允許多個shared_ptr類型指針指向同一個對象;unique_ptr 則 “獨占” 所指向的對象。標准庫還定義了一個名為 weak_ptr 的伴隨類,它是一種弱引用,指向 shared_ptr 所管理的對象。這三種類型都定義在 memory 頭文件中。
一、shared_ptr 類
類似 vector,智能指針也是模板。因此,當定義智能指針時,必須在尖括號內給出類型,如下所示:
shared_ptr<string> p1; // shared_ptr,可以指向string類型的對象
shared_ptr<list<int>> p1; // shared_ptr,可以指向int類型的list的對象
默認初始化的智能指針中保存着一個空指針。智能指針的使用方式與普通指針類似,解引用一個智能指針返回它指向的對象。
下面列出了 shared_ptr 和 unique_ptr 都支持的操作。
shared_ptr<T> sp // 空shared_ptr智能指針,可以指向類型為T的對象
unique_ptr<T> up // 空unique_ptr智能指針,可以指向類型為T的對象
p // 將p用作一個條件判斷,若p指向一個對象,則為ture
*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,指向一個動態分配的類型為T的對象。使用args初始化此對象
shared_ptr<T> p(q) //p是shared_ptr q的拷貝;此操作會遞增q中的引用計數。q中的指針必須能轉換成T*
p = q //p和q都是shared_ptr,所保存的指針必須能相互轉換。此操作會遞減p中的引用計數,遞增q中的引用計數。若p中的引用計數變為0,則將其管理的原內存釋放
p.unique() //若p.use_count()為1,返回true;否則返回false
p.use_count() //返回與p共享對象的智能指針數量;可能很慢,主要用於調試
下面介紹一些改變 shared_ptr 的其他方法:
p.reset () //若p是唯一指向其對象的shared_ptr,reset會釋放此對象。
p.reset(q) //若傳遞了可選的參數內置指針q,會令P指向q,否則會將P置為空。
p.reset(q, d) //若還傳遞了參數d,將會調用d而不是delete 來釋放q
1. 使用 make_shared 函數分配內存並返回 shared_ptr 指針
最安全的分配和使用動態內存的方法是調用一個名為 make_shared 的標准庫函數。 此函數在動態內存中分配一個對象並初始化它,返回指向此對象的 shared_ptr。與智能指針一樣,make_shared 也定義在頭文件 memory 中。
當要用 make_shared 時,必須指定想要創建的對象的類型。定義方式與模板類相同, 在函數名之后跟一個尖括號,在其中給出類型:
// 指向一個值為42的int的shared_ptr
shared_ptr<int> p3 = make_shared<int>(42);
// p4 指向一個值為"9999999999"的string
shared_ptr<string> p4 = make_shared<string>(10,'9');
// p5指向一個值初始化的int
shared_ptr<int> p5 = make_shared<int>();
當然,我們通常用 auto 定義一個對象來保存 make_shared 的結果,這種方式較為簡單:
// p6指向一個動態分配的空vector<string>
auto p6 = make_shared<vector>();
2. shared_ptr 的拷貝和賦值
我們可以認為每個 shared_ptr 都有一個關聯的計數器,通常稱其為引用計數(reference count)。無論何時我們拷貝一個 shared_ptr,例如,當用一個 shared_ptr 初始化另一個 shared_ptr,或將它作為參數傳遞給一個函數以及作為函數的返回值時,它所關聯的引用計數就會遞增。而當我們給 shared_ptr 賦予一個新值或者 shared_ptr 被銷毀時,引用計數就會遞減。
一旦一個 shared_ptr 的計數器變為 0,它就會自動釋放自己所管理的對象:
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原來指向的對象已沒有引用者,會自動釋放
此例中我們分配了一個 int,將其指針保存在 r 中。接下來,我們將一個新值賦予 r。在此情況下,r 是唯一指向此 int 的 shared_ptr,在把 q 賦給 r 的過程中,此 int 被自動釋放。
3. shared_ptr 自動銷毀所管理的對象……
當指向一個對象的最后一個 shared_ptr 被銷毀時,Shared_ptr 類會自動銷毀此 對象。它是通過另一個特殊的成員函數—析構函數完成銷毀工作的。shared_ptr 的析構函數會遞減它所指向的對象的引用計數。如果引用計數變為 0,shared_ptr 的析構函數就會銷毀對象,並釋放它占用的內存。
4. shared_ptr 和 new 結合使用
我們還可以用 new 返回的指針來初始化智能指針,如下所示:
shared_ptr<int> p2(new int (42)); // p2 指向一個值為 42 的 int
接受指針參數的智能指針構造函數是 explicit 的。因此,我們不能將一個內置指針隱式轉換為一個智能指針,必須使用直接初始化形式來初始化一個智能指針:
shared_ptr<int> pi = new int (1024); // 錯誤:必須使用直接初始化形式
shared_ptr<int> p2(new int(1024)); // 正確:使用了直接初始化形式
出於相同的原因,一個返回 shared_ptr 的函數不能在其返回語句中隱式轉換一個普通指針:
shared_ptr<int> clone(int p)
{
return new int(p); // 錯誤:隱式轉換為 shared_ptr<int>
}
我們必須將 shared_ptr 顯式綁定到一個想要返回的指針上:
shared_ptr<int> clone(int p)
{
return shared_ptr<int>(new int(p)); // 正確:顯式地用int*創建shared_ptr<int>
}
5. 不要混合使用普通指針和智能指針
使用一個內置指針來訪問一個智能指針所負責的對象是很危險的,因為我們無法知道對象何時會被銷毀。 |
考慮下面對 shared_ptr 進行操作的函數:
//在函數被調用時 ptr 被創建並初始化
void process(shared_ptr<int> ptr)
{
//使用ptr
}//ptr離開作用域,被銷毀
int main()
{
shared_ptr<int> p( new int (42) ) ; //引用計數為 1
process (p);//拷貝p會遞增它的引用計數;在process中引用計數值為2
int i = *p; //正確:引用計數值為1
}
下面考慮混合使用普通指針和智能指針的情況。雖然不能傳遞給 process —個內置指針,但可以傳遞給它一個(臨時的) shared_ptr,這個 shared_ptr 是用一個內置指針顯式構造的。但是,這樣做很可能會導致錯誤:
int *x(new int(1024)); // 危險:x是一個普通指針,不是一個智能指針
process (x);// 錯誤:不能將 int*轉換為一個 shared_ptr<int>
process ( shared_ptr<int> (x) ); // 合法的,但內存會被釋放!
int j = *x; // 未定義的:x是一個空懸指針!
在上面的調用中,我們將一個臨時 shared_ptr 傳遞給 process。當這個調用所在的表達式結束時,這個臨時對象就被銷毀了。銷毀這個臨時變量會遞減引用計數,此時引用計數就變為 0 了。因此,當臨時對象被銷毀時,它所指向的內存會被釋放。但 x 繼續指向(已經釋放的)內存,從而變成一個空懸指針。如果試圖使用 x 的值,其行為是未定義的。
二、unique_ptr 類
一個 unique_ptr “擁有” 它所指向的對象。與 shared_ptr 不同,某個時刻只能有一個 unique_ptr 指向一個給定對象。當 unique_ptr 被銷毀時,它所指向的對象也被銷毀。
與 shared_ptr 不同,沒有類似 make_shared 的標准庫函數返回一個 unique_ptr。當我們定義一個 unique_ptr 時,需要將其綁定到一個 new 返回的指針上。類似 shared_ptr,初始化 unique_ptr 必須采用直接初始化形式:
unique_ptr<double> p1; // 指向一個double的unique_ptr
unique_ptr<double> p2(new int(42)); // p2指向一個值為42的int
由於一個 unique_ptr 擁有它指向的對象,因此 unique_ptr 不支持普通的拷貝或賦值操作:
unique_ptr<string> p1(new string("Stegosaurus"));
unique_ptr<string> p2 (p1); // 錯誤:unique_ptr 不支持拷貝
unique_ptr<string> p3;
p3 = p2; // 錯誤:unique_ptr不支持賦值
下面列出了 unique_ptr 特有的操作。
unique_ptr<T> u1 // 空unique_ptr,可以指向類型為T的對象。u1會使用delete來釋放它的指針
unique_ptr<T, D> u2 // u2會使用一個類型為D的可調用對象來釋放它的指針
unique_ptr<T, D> u(d) // 空unique_ptr,指向類型為T的對象,用類型為D的對象d替代delete
u = nullptr // 釋放u指向的對象,將u置為空
u.release() // u放棄對指針的控制權,返回指針,並將u置為空
u.reset() // 釋放u指向的對象
u.reset(q) // 如果提供了內置指針q,另u指向這個對象;否則將u置為空'
u.reset(nullptr)
雖然我們不能拷貝或賦值 unique_ptr,但可以通過調用 release 或 reset 將指針的 所有權從一個(非const)unique_ptr 轉移給另一個 unique:
// 將所有權從pl (指向string Stegosaurus)轉移給p2
unique_ptr<string> p2(p1, release()); // release 將 p1 置為空
unique_ptr<string> p3(new string("Trex"));
// 將所有權從p3轉移給p2
p2.reset(p3.release()); // reset 釋放了 p2 原來指向的內存
release 成員返回 unique_ptr 當前保存的指針並將其置為空。因此,p2 被初始化為 p1 原來保存的指針,而 p1 被置為空。
調用 release 會切斷 unique_ptr 和它原來管理的對象間的聯系,如果我們不用另一個智能指針來保存 release 返回的指針,我們的程序就要負責資源的釋放:
p2.release(); // 錯誤:p2不會釋放內存,而且我們丟失了指針
auto p = p2.release(); // 正確,但我們必須記得 delete(p)
傳遞unique_ptr參數和返回unique_ptr
不能拷貝unique_ptr的規則有一個例外:我們可以拷貝或賦值一個將要被銷毀的 unique_ptr。最常見的例子是從函數返回一個unique_ptr:
unique_ptr<int> clone(int p)
{
// 正確:從 int*創建一個 unique_ptr<int>
return unique_ptr<int>(new int(p));
}
還可以返回一個局部對象的拷貝:
unique_ptr<int> clone (int p)
{
unique_ptr<int> ret(new int (p));
//…
return ret;
}
對於上面兩段代碼,編譯器都知道要返回的對象將要被銷毀。在此情況下,編譯器執行一種特殊的“拷貝”,在《C++ Primer》13.6.2節(第473頁)中有介紹。
三、weak_ptr 類
weak_ptr 是一種不控制所指向對象生存期的智能指針,它指向由一個 shared_ptr 管理的對象。將一個 weak_ptr 綁定到一個 shared_ptr 不會改變 shared_ptr 的引用計數。一旦最后一個指向對象的 shared_ptr 被銷毀,對象就會被釋放。即使有 weak_ptr 指向對象,對象也還是會被釋放,因此,weak_ptr 的名字抓住了這種智能指針 “弱” 共享對象的特點。
下面列出了 weak_ptr 的操作。
weak_ptr<T> w // 空weak_ptr可以指向類型為T的對象
weak_ptr<T> w(sp) // 與shared_ptr sp指向相同對象的weak_ptr。T必須能轉換為sp指向的S型
w = p // p可以是一個shared_ptr或一個weak_ptr。賦值后w與p共享對象
w.reset() // 將W置為空
w.use_count() // 與w共享對象的shared ptr的數量
w.expired() // 若 w.use_count()為0,返回true,否貝y返回 false
w.lock() // 如果expired為true,返回一個空shared ptr:否則返回一個 指向w的對象的shared_ptr
當我們創建一個 weak_ptr 時,要用一個 shared_ptr 來初始化它:
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p; p的引用計數未改變
本例中 wp 和 p 指向相同的對象。由於是弱共享,創建 wp 不會改變 p 的引用計數;wp 指向的對象可能被釋放掉。
由於對象可能不存在,我們不能使用 weak_ptr 直接訪問對象,而必須調用 lock。 此函數檢查 weak_ptr 指向的對象是否仍存在。如果存在,lock 返回一個指向共享對象的 shared_ptr。與任何其他 shared_ptr 類似,只要此 shared_ptr 存在,它所指向的底層對象也就會一直存在。例如:
if ( shared_ptr<int> np = wp.lock() )
{
// 如果 np 不為空則條件成立
// 在if中,np與p共享對象
}
在這段代碼中,只有當 lock 調用返回 true 時我們才會進入 if 語句體。在if中,使用 np 訪問共享對象是安全的。
參考
《C++ Primer 第5版》