C++智慧指標:shared_ptr,uniqe_ptr,weak_ptr
動態記憶體
在C++中,動態記憶體的管理是通過一對運算子來完成的:new和delete。
new:在動態記憶體中為物件分配空間,並返回一個指向該物件的指標
delete:接受一個動態物件的指標,銷燬該物件,並釋放與之關聯的記憶體
動態記憶體的使用需要十分小心,因為要在程式設計的時候要確保在正確的時間對記憶體進行釋放是極其困難的。如果釋放記憶體不及時,在這種情況下就會出現記憶體洩漏;但若過早的釋放(在仍有指標指向該記憶體的時候就把其釋放了),就會產生引用非法記憶體的指標(稱為空懸指標)。
程式使用動態記憶體主要處以以下三種原因之一:
- 程式不知道自己需要多少物件
- 程式不知道所需物件的準確型別
- 程式要在多個物件間共享資料(多個物件共享相同底層資料)
在C++中,為了能更安全更容易的使用動態記憶體,新的標準庫提供了兩種智慧指標(smart pointer)型別來管理動態物件(注意:智慧指標是型別)。
智慧指標
智慧指標的行為類似常規指標,但與常規不同的是,它負責自動釋放所指向物件。
新標準庫提供的這兩種智慧指標的區別在於管理底層指標的方式:
shared_ptr:允許多個指標指向同一個物件
imoqie_ptr:“獨佔”所指向的物件
同時,標準庫還提供了一個名為weak_ptr的伴隨類:
weak_ptr:它是一種弱引用,指向shared_ptr所管理的物件。
以上三種類型都定義在memory標頭檔案中。
shared_ptr類
shared_ptr類類似vector,智慧指標也是一種模板(template)。因此在建立智慧指標物件的時候,也必須提供指標指向的型別的資訊。
shared_ptr<int> p1;
shared_ptr<string> p2;
shared_ptr<int> p3 (new int(16)); //正確
shared_ptr<int> p4 = new int(16); //錯誤
對於p3,p4,shared_ptr的建構函式是explicit的,因此我們不能將一個內建指標隱式轉換為一個智慧指標(p4的初始化隱式地要求編譯器用一個new返回的int*來建立一個shared_ptr,因此是錯誤的)。
shared_ptr和unique_ptr都支援的操作 | |
---|---|
shared_ptr< T >sp | 空智慧指標,預設初始化的智慧指標中儲存著一個空指標 |
p | 將p作為一個條件判斷,若p指向一個物件,則為true |
*p | 解引用p,獲得p指向的物件 |
p->mem | 等價於(*p).mem |
p.get() | 返回p中儲存的指標。小心使用,若智慧指標釋放了其物件,返回的指標所指向的物件也同時消失了。 |
swap(p,q) | 交換p和q的指標 |
p.swap(q) | 交換p和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和都為shared_ptr,儲存的指標必須能相互轉換,此操作會遞減p的引用計數,遞增q的引用計數;若p的引用計數為0,則將其管理的記憶體釋放 |
p.unique() | 若p.use_count()為1,返回true,否則false |
p.use_count() | 返回與p共享物件的智慧指標數量(只要勇除錯) |
定義和改變shared_ptr的其他方法 | |
---|---|
shared_ptr< T > p(q) | p管理內建指標q所指向的物件,q必須質量new分配的記憶體切可以轉換為T*內型 |
shared_ptr< T > p(p2,d) | p是p2的拷貝,唯一區別是p將使用可呼叫物件d來代替delete(lambda表示式) |
shared_ptr< T > p(u) | p從unique_ptr u處接管了物件所有權,將u置為空 |
shared_ptr< T > p(q,d) | p將使用可呼叫物件d來代替delete(lambda表示式) |
p.reset() | p若是唯一指向器物件的shared_ptr,reset將釋放此物件 |
p.reset(q) | 若釋放後,傳遞了q,會令p指向q |
p.reset(q,d) | 結合以上兩種方法,使用lambda表示式用d代替delete |
對於shared_ptr< T >p(q)的操作 和 p=q的拷貝和賦值操作,我們要注意個物件中的計數器的變化和引用計數(reference count)的變化。
我們用一個shared_ptr去初始化另一個shared_ptr,或將它作為引數傳遞給一個函式以及作為函式的返回值,它所關聯的計數器就會遞增。
若給shared_ptr賦予一個新值或者是shared_ptr被銷燬,一個區域性的shared_ptr離開了作用域,計數器就會遞減。
make_shared 函式
make_shared同樣也包含在memory標頭檔案中。
最安全的分配和使用動態記憶體的方法是呼叫一個名為make_shared 的標準庫函式。
make_shared函式在動態記憶體中分配一個物件並初始化它,返回指向此物件的shared_ptr。
//p1為一個指向值為999的int的shared_ptr
shared_ptr<int>p1 = make_shared<int>(999);
//p2為指向一個值初始化的int(即此int值為0)
shared_ptr<int>p2 = make_shared<int>();
p2看出,如果傳遞任何引數,物件就會進行值初始化。
shared_ptr自動銷燬所管理的物件,自動釋放相關聯的記憶體
當指向一個物件的最後一個shared_ptr被銷燬,shared_ptr類會自動銷燬這個物件。他通過他的解構函式完成這項工作。
若當動態物件不被使用,shared_ptr類會自動釋放動態物件,這個特性就是的動態記憶體的使用變得十分容易。
例如一下的factory 函式:
shared_ptr<FOO> factory (T arg)
{
//函式內容
//shared_ptr負責釋放記憶體
return make_shared<FOO>(arg);
}
我們在use_factory函式呼叫它:
void use_factory(T arg)
{
shared_ptr<FOO>p = factory(arg);
//函式內容
}
//在這裡,p屬於區域性的變數,p離開了作用域,它指向的記憶體也會被自動釋放
p屬於區域性的變數,p離開了作用域,它指向的記憶體也會被自動釋放。當p被銷燬時,將遞減引用計數並檢查他是否為0。
但如果有其他的shared_ptr也同時指向這塊記憶體,他將不會被釋放(引用計數器檢查不為0)
void use_factory(T arg)
{
shared_ptr<FOO>p = factory(arg);
//(此例此處可以簡單的看作引用計數為1)
//函式內容
return p;
//函式反悔了p,引用計數進行了遞增操作(此處可看作引用計數為2)
}
//p離開了作用域,引用計數遞減但最終但不為0(引用計數為1)
和其特性一樣,若引用計數不為0,他的記憶體都不會被釋放,因此保證shared_ptr在無用之後不再保留非常重要(避免記憶體浪費)。
一種要注意的情況就是,如果將shared_ptr儲存在一個容器中(如vector),隨後重拍了容器,不再需要其中某些元素,這種情況下,要確保用erase函式刪除那些不再需要的shared_ptr。
unique_ptr類
unique_ptr“擁有”它所指向的物件,與shared_ptr不同,某個時刻只能由一個unique_ptr指向一個給定物件。
和shared_ptr不同的是,沒有類似make_shared的標準庫函式返回一個unique_str。
我們要定義一個unique_ptr時,需要將其繫結到一個new返回的指標上,類似shared_ptr,初始化unique_ptr必須採用直接初始化形式(unique_ptr建構函式也為explicit)。
unique_ptr<string> p1(new string ("ABC");
unique_ptr<string> p2(p1); //錯誤,unique_ptr不支援拷貝
unique_ptr<string> p2(p1.release()); //正確,up1用realease置為空,p2初始化為p1原來的指標
unique_ptr<string> p3;
p3 = p2; //錯誤,unique_ptr不支援賦值
p2.reset(p1.release()); //正確,reset釋放了p2指向的記憶體,這行程式碼表示將所有權從p1轉移給p2
unique_ptr操作(與shared_ptr相同操作不重複列出) | |
---|---|
unique_ptr< T >u1 | 空unique_ptr,指向型別為T物件。 |
unique_ptr< T , D> u2 | 空unique_ptr,與u1不同的是,u2會使用一個型別為D的可呼叫物件來釋放他的指標 |
unique_ptr< T , D > u(d) | 空unique_ptr,會使用型別為D的物件d來代替delete |
u = nullptr | 釋放u所指向物件,u置為空 |
u.realease() | 放棄對指標控制權,返回指標並將u置為空 |
u.reset() | 釋放u所指向的物件 |
u.reset(q) | 若提供了內建指標q,會令u指向q |
u.reset(nullptr) |
weak_ptr類
weak_ptr是一種不控制所指向物件生存期的正能指標,他指向一個shared_ptr管理的物件。
將一個weak_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所指向的型別 |
w = p | p可以是一個shared_ptr也可以是weak_ptr,賦值後p與w共享物件 |
w.rese() | w置為空 |
w.use_count() | 與w共享物件的shared_ptr的數量 |
w.expired() | 若w.use_count()為0,返回true,否則false |
w.lock() | 如果expired為true,返回一個空shared_ptr,否則返回一個指向w的物件的shared_ptr |
auto p = make_shared<int>(666);
weak_ptr<int> wp(p);
//wp弱共享p,不會改變p引用計數
在使用weak_ptr時,由於指向的物件可能不存在,因此需要呼叫lock來檢查weak_ptr所指向物件是否存在,如:
if(shared_ptr<int> np== wp.lock()){ //lock在指向物件存在時返回一個指向該物件的shared_ptr
//在if中,np與wp共享物件
//但僅僅可以保證在此if中,該物件的共享訪問是安全的
}