c++ 智能指針(轉)
智能指針是在 <memory> 標頭文件中的 std
命名空間中定義的。 它們對 RAII 或“獲取資源即初始化”編程慣用法至關重要。 此習慣用法的主要目的是確保資源獲取與對象初始化同時發生,從而能夠創建該對象的所有資源並在某行代碼中準備就緒。
實際上,RAII 的主要原則是為將任何堆分配資源(例如,動態分配內存或系統對象句柄)的所有權提供給其析構函數包含用於刪除或釋放資源的代碼以及任何相關清理代碼的堆棧分配對象。
大多數情況下,當初始化原始指針或資源句柄以指向實際資源時,會立即將指針傳遞給智能指針。 在現代 C++ 中,原始指針僅用於範圍有限的小代碼塊、循環或者性能至關重要且不會混淆所有權的 Helper 函數中。
下面的示例將原始指針聲明與智能指針聲明進行了比較。
void UseRawPointer() { // Using a raw pointer -- not recommended. Song* pSong = new Song(L"Nothing on You", L"Bruno Mars"); // Use pSong... // Don‘t forget to delete! delete pSong; } void UseSmartPointer() { // Declare a smart pointer on stack and pass it the raw pointer. unique_ptr<Song> song2(new Song(L"Nothing on You", L"Bruno Mars")); // Use song2... wstring s = song2->duration_; //... } // song2 is deleted automatically here.
如示例所示,智能指針是你在堆棧上聲明的類模板,並可通過使用指向某個堆分配的對象的原始指針進行初始化。 在初始化智能指針後,它將擁有原始的指針。 這意味著智能指針負責刪除原始指針指定的內存。
智能指針析構函數包括要刪除的調用,並且由於在堆棧上聲明了智能指針,當智能指針超出範圍時將調用其析構函數,盡管堆棧上的某處將進一步引發異常。
通過使用熟悉的指針運算符(->
和 *
)訪問封裝指針,智能指針類將重載這些運算符以返回封裝的原始指針。
C++ 智能指針思路類似於在語言(如 C#)中創建對象的過程:創建對象後讓系統負責在正確的時間將其刪除。 不同之處在於,單獨的垃圾回收器不在後臺運行;按照標準 C++ 範圍規則對內存進行管理,以使運行時環境更快速更有效。
重要事項:請始終在單獨的代碼行上創建智能指針,而絕不在參數列表中創建智能指針,這樣就不會由於某些參數列表分配規則而發生輕微泄露資源的情況
下面的示例演示了如何使用標準模板庫中的 unique_ptr
智能指針類型將指針封裝到大型對象。
class LargeObject { public: void DoSomething(){} }; void ProcessLargeObject(const LargeObject& lo){} void SmartPointerDemo() { // Create the object and pass it to a smart pointer std::unique_ptr<LargeObject> pLarge(new LargeObject()); //Call a method on the object pLarge->DoSomething(); // Pass a reference to a method. ProcessLargeObject(*pLarge); } //pLarge is deleted automatically when function block goes out of scope.
此示例演示如何使用智能指針執行以下關鍵步驟。
-
將智能指針聲明為一個自動(局部)變量。(不要對智能指針本身使用
new
或malloc
表達式。) -
在類型參數中,指定封裝指針的指向類型。
-
在智能指針構造函數中將原始指針傳遞至
new
對象。(某些實用工具函數或智能指針構造函數可為你執行此操作。) -
使用重載的
->
和*
運算符訪問對象。 -
允許智能指針刪除對象。
智能指針的設計原則是在內存和性能上盡可能高效。 例如,unique_ptr
中的唯一數據成員是封裝的指針。 這意味著,unique_ptr
與該指針的大小完全相同,不是四個字節就是八個字節。
使用重載了 * 和 -> 運算符的智能指針訪問封裝指針的速度不會明顯慢於直接訪問原始指針的速度。
智能指針具有通過使用“點”表示法訪問的成員函數。 例如,一些 STL 智能指針具有釋放指針所有權的重置成員函數。 如果你想要在智能指針超出範圍之前釋放其內存將很有用,這會很有用,如以下示例所示:
void SmartPointerDemo2() { // Create the object and pass it to a smart pointer std::unique_ptr<LargeObject> pLarge(new LargeObject()); //Call a method on the object pLarge->DoSomething(); // Free the memory before we exit function block. pLarge.reset(); // Do some other work... }
智能指針通常提供直接訪問其原始指針的方法。 STL 智能指針擁有一個用於此目的的 get
成員函數,CComPtr
擁有一個公共的 p
類成員。
通過提供對基礎指針的直接訪問,你可以使用智能指針管理你自己的代碼中的內存,還能將原始指針傳遞給不支持智能指針的代碼。
void SmartPointerDemo4() { // Create the object and pass it to a smart pointer std::unique_ptr<LargeObject> pLarge(new LargeObject()); //Call a method on the object pLarge->DoSomething(); // Pass raw pointer to a legacy API LegacyLargeObjectFunction(pLarge.get()); }
智能指針的類型
C++ 標準庫智能指針
使用這些智能指針作為將指針封裝為純舊 C++ 對象 (POCO) 的首選項。
unique_ptr
只允許基礎指針的一個所有者。 除非你確信需要 shared_ptr
,否則請將該指針用作 POCO 的默認選項。 可以移到新所有者,但不會復制或共享。替換已棄用的 auto_ptr
。 與 boost::scoped_ptr
比較。 unique_ptr
小巧高效;大小等同於一個指針且支持 rvalue 引用,從而可實現快速插入和對 STL 集合的檢索。 頭文件:<memory>
。
unique_ptr 不共享它的指針。 它無法復制到其他 unique_ptr
,無法通過值傳遞到函數,也無法用於需要副本的任何標準模板庫 (STL) 算法。 只能移動unique_ptr
。 這意味著,內存資源所有權將轉移到另一 unique_ptr
,並且原始 unique_ptr
不再擁有此資源。 我們建議你將對象限制為由一個所有者所有,因為多個所有權會使程序邏輯變得復雜。 因此,當需要智能指針用於純 C++ 對象時,可使用 unique_ptr
,而當構造 unique_ptr
時,可使用make_unique Helper 函數。
下圖演示了兩個 unique_ptr
實例之間的所有權轉換。
unique_ptr
在 STL 的 <memory>
標頭中定義。 它與原始指針一樣有效,並可用於 STL 容器。 將 unique_ptr
實例添加到 STL 容器很有效,因為通過 unique_ptr
的移動構造函數,不再需要進行復制操作。
示例1:
以下示例演示如何創建 unique_ptr
實例並在函數之間傳遞這些實例。
unique_ptr<Song> SongFactory(const std::wstring& artist, const std::wstring& title) { // Implicit move operation into the variable that stores the result. return make_unique<Song>(artist, title); } void MakeSongs() { // Create a new unique_ptr with a new object. auto song = make_unique<Song>(L"Mr. Children", L"Namonaki Uta"); // Use the unique_ptr. vector<wstring> titles = { song->title }; // Move raw pointer from one unique_ptr to another. unique_ptr<Song> song2 = std::move(song); // Obtain unique_ptr from function that returns by value. auto song3 = SongFactory(L"Michael Jackson", L"Beat It"); }
這些示例說明了 unique_ptr
的基本特征:可移動,但不可復制。“移動”將所有權轉移到新 unique_ptr
並重置舊 unique_ptr
。
unique_ptr
實例並在向量中使用這些實例。
void SongVector() { vector<unique_ptr<Song>> songs; // Create a few new unique_ptr<Song> instances // and add them to vector using implicit move semantics. songs.push_back(make_unique<Song>(L"B‘z", L"Juice")); songs.push_back(make_unique<Song>(L"Namie Amuro", L"Funky Town")); songs.push_back(make_unique<Song>(L"Kome Kome Club", L"Kimi ga Iru Dake de")); songs.push_back(make_unique<Song>(L"Ayumi Hamasaki", L"Poker Face")); // Pass by const reference when possible to avoid copying. for (const auto& song : songs) { wcout << L"Artist: " << song->artist << L" Title: " << song->title << endl; } }
在 range for 循環中,註意 unique_ptr
通過引用來傳遞。 如果你嘗試通過此處的值傳遞,由於刪除了 unique_ptr
復制構造函數,編譯器將引發錯誤。
示例3:
以下示例演示如何初始化類成員 unique_ptr
。
class MyClass { private: // MyClass owns the unique_ptr. unique_ptr<ClassFactory> factory; public: // Initialize by using make_unique with ClassFactory default constructor. MyClass() : factory ( make_unique<ClassFactory>()) { } void MakeClass() { factory->DoSomething(); } };
示例4:
可使用 make_unique 將 unique_ptr
創建到數組,但無法使用 make_unique
初始化數組元素。
// Create a unique_ptr to an array of 5 integers. auto p = make_unique<int[]>(5); // Initialize the array. for (int i = 0; i < 5; ++i) { p[i] = i; wcout << p[i] << endl; }
shared_ptr
采用引用計數的智能指針。 如果你想要將一個原始指針分配給多個所有者(例如,從容器返回了指針副本又想保留原始指針時),請使用該指針。 直至所有 shared_ptr
所有者超出了範圍或放棄所有權,才會刪除原始指針。 大小為兩個指針;一個用於對象,另一個用於包含引用計數的共享控制塊。 頭文件:<memory>
。
shared_ptr
的類型是C + +標準庫中一個聰明的指針,是為多個擁有者管理內存中對象的生命周期而設計的。 在你初始化一個 shared_ptr
後,你可以復制它,把函數參數的值遞給它,並把它分配給其它 shared_ptr
實例。 所有實例指向同一個對象,並共享訪問一個“控制塊”,即每當一個新的shared_ptr
被添加時,遞增和遞減引用計數,超出範圍,則復位。 當引用計數到達零時,控制塊刪除內存資源和自身。
下圖顯示了指向一個內存位置的幾個 shared_ptr
實例。
shared_ptr
。 make_shared
異常安全。 它使用同一調用分配的內存控制塊和資源從而減少構造開銷。 如果你不使用 make_shared
,那麽在把它傳遞給 shared_ptr
的構造函數之前,你必須使用一個明確的新表達式創建的對象。 下面的例子顯示了在新對象中聲明和初始化一個 shared_ptr
的各種方式。
// Use make_shared function when possible. auto sp1 = make_shared<Song>(L"The Beatles", L"Im Happy Just to Dance With You"); // Ok, but slightly less efficient. // Note: Using new expression as constructor argument // creates no named variable for other code to access. shared_ptr<Song> sp2(new Song(L"Lady Gaga", L"Just Dance")); // When initialization must be separate from declaration, e.g. class members, // initialize with nullptr to make your programming intent explicit. shared_ptr<Song> sp5(nullptr); //Equivalent to: shared_ptr<Song> sp5; //... sp5 = make_shared<Song>(L"Elton John", L"I‘m Still Standing");
示例2:
下面的示例演示如何聲明和初始化一個已經被分配了另一個 shared_ptr
的對象共享所有權的 shared_ptr
的實例。 假設 sp2
是一個初始化的shared_ptr
。
//Initialize with copy constructor. Increments ref count. auto sp3(sp2); //Initialize via assignment. Increments ref count. auto sp4 = sp2; //Initialize with nullptr. sp7 is empty. shared_ptr<Song> sp7(nullptr); // Initialize with another shared_ptr. sp1 and sp2 // swap pointers as well as ref counts. sp1.swap(sp2);
示例3:
當您使用算法復制元素時,shared_ptr
的也是很有用的標準模板庫(STL)。 你可以把元素包裝在 shared_ptr
裏,然後將其復制到其他容器,只要你需要它,底層的內存始終是有效的。 以下示例演示如何使用 replace_copy_if
算法來創建一個 shared_ptr
的實例以及如何在一個向量上進行使用。
vector<shared_ptr<Song>> v; v.push_back(make_shared<Song>(L"Bob Dylan", L"The Times They Are A Changing")); v.push_back(make_shared<Song>(L"Aretha Franklin", L"Bridge Over Troubled Water")); v.push_back(make_shared<Song>(L"Thal?a", L"Entre El Mar y Una Estrella")); vector<shared_ptr<Song>> v2; remove_copy_if(v.begin(), v.end(), back_inserter(v2), [] (shared_ptr<Song> s) { return s->artist.compare(L"Bob Dylan") == 0; }); for (const auto& s : v2) { wcout << s->artist << L":" << s->title << endl; }
示例4: 你可以用
dynamic_pointer_cast
, static_pointer_cast
和 const_pointer_cast
來轉換shared_ptr
。 這些函數的操作類似 dynamic_cast
, static_cast
和 const_cast
。 下面的示例演示如何測試在基類的 shared_ptr
向量中的每個元素的派生類,,然後復制元素,並顯示它們的信息。
vector<shared_ptr<MediaAsset>> assets; assets.push_back(shared_ptr<Song>(new Song(L"Himesh Reshammiya", L"Tera Surroor"))); assets.push_back(shared_ptr<Song>(new Song(L"Penaz Masani", L"Tu Dil De De"))); assets.push_back(shared_ptr<Photo>(new Photo(L"2011-04-06", L"Redmond, WA", L"Soccer field at Microsoft."))); vector<shared_ptr<MediaAsset>> photos; copy_if(assets.begin(), assets.end(), back_inserter(photos), [] (shared_ptr<MediaAsset> p) -> bool { // Use dynamic_pointer_cast to test whether // element is a shared_ptr<Photo>. shared_ptr<Photo> temp = dynamic_pointer_cast<Photo>(p); return temp.get() != nullptr; }); for (const auto& p : photos) { // We know that the photos vector contains only // shared_ptr<Photo> objects, so use static_cast. wcout << "Photo location: " << (static_pointer_cast<Photo>(p))->location_ << endl; }
示例5:
你可以用下列方法把 shared_ptr
傳遞給另一個函數:
-
向
shared_ptr
傳遞值。 調用復制構造函數,遞增引用計數,並把被調用方當做所有者。 還有就是在這次操作中有少量的開銷,這很大程度上取決於你傳遞了多少shared_ptr
對象。 當調用方和被調用方之間的代碼協定 (隱式或顯式) 要求被調用方是所有者,使用此選項。 -
通過引用或常量引用來傳遞
shared_ptr
。 在這種情況下,引用計數不增加,並且只要調用方不超出範圍,被調用方就可以訪問指針。 或者,被調用方可以決定創建一個基於引用的shared_ptr
,從而成為一個共享所有者。 當調用者並不知道被被調用方,或當您必須傳遞一個shared_ptr
,並希望避免由於性能原因的復制操作,請使用此選項。 -
通過底層的指針或引用底層的對象。 這使得被調用方使用對象,但不使共享所有權或擴展生存期。 如果被調用方從原始指針創建一個
shared_ptr
,則新的shared_ptr
是獨立於原來的,且沒有控制底層的資源。 當調用方和被調用方之間的協定中明確規定調用者保留shared_ptr
生存期的所有權,則使用此選項。 -
當您決定如何傳遞一個
shared_ptr
時,確定被調用方是否有共享基礎資源的所有權。 一個“所有者”就是只要它需要就可以使用底層資源的對象或函數。 如果調用方必須保證被調用方可以在其(函數)生存期以外擴展指針的生存期,請使用第一個選項。 如果您不關心被調用方是否擴展生存期,則通過引用傳遞並讓被調用方復制它。 -
如果不得不允許幫助程序函數訪問底層指針,並且您知道幫助程序函數將使用指針且在調用函數返回前先返回,則該函數不必共享底層指針的所有權。 僅僅是在調用方的
shared_ptr
的生存期內允許訪問指針。 在這種情況下,通過引用來傳遞shared_ptr
,通過原始指針或引用的基本對象都是安全的。 通過此方式提供一個小的性能改進,並且還有助於表示程序的意圖。 -
有時,例如在一個
std:vector<shared_ptr<T>>
中,您可能必須對傳遞每個shared_ptr
給lambda表達式體或命名函數對象。 如果lambda或函數沒有存儲指針,則通過引用傳遞shared_ptr
,以避免調用拷貝構造函數的每個元素。
weak_ptr
結合 shared_ptr
使用的特例智能指針。 weak_ptr
提供對一個或多個 shared_ptr
實例擁有的對象的訪問,但不參與引用計數。 如果你想要觀察某個對象但不需要其保持活動狀態,請使用該實例。 在某些情況下,需要斷開 shared_ptr
實例間的循環引用。 頭文件:<memory>
。
有時對象必須存儲一種方法,用來在不引起引用計數增加的情況下訪問 shared_ptr
的基礎對象。 通常,當您在 shared_ptr
實例之間循環引用時,就會出現此情況。
最佳的設計能夠盡可能地避免指針具有共享所有權。 但是,如果您必須具有共享的 shared_ptr
實例所有權,請避免在實例之間進行循環引用。 如果循環引用不可避免,甚至由於某種原因而更為可取,請使用 weak_ptr
為一個或多個所有者提供對其他 shared_ptr
的弱引用。 使用 weak_ptr
,您可以創建連接到現有相關實例組的 shared_ptr
,但僅當基礎內存資源有效時才行。 weak_ptr
本身並不參與引用計數,因此,它無法阻止引用計數轉到為零。 但是,您可以使用 weak_ptr
來嘗試獲取 shared_ptr
的新副本,通過使用該副本進行初始化。 如果內存已被刪除,則會引發 bad_weak_ptr 異常。 如果內存仍有效,則新的共享指針會遞增引用計數,並確保只要 shared_ptr
變量保持在範圍內,內存就有效。
weak_ptr
以確保正確刪除循環依賴關系對象的實例。 檢查示例時,假定它是僅在考慮備用解決方案後才創建的。Controller
對象表示設備處理的某個方面,並且能獨立運行。 每個控制器必須能夠在任何時間查詢其他控制器的狀態,因此,每個控制器包含私有vector<weak_ptr<Controller>>
。 由於每個向量包含一個循環引用,因此使用 weak_ptr
實例而不是 shared_ptr
。
#include <iostream> #include <memory> #include <string> #include <vector> #include <algorithm> using namespace std; class Controller { public: int Num; wstring Status; vector<weak_ptr<Controller>> others; explicit Controller(int i) : Num(i) , Status(L"On") { wcout << L"Creating Controller" << Num << endl; } ~Controller() { wcout << L"Destroying Controller" << Num << endl; } // Demonstrates how to test whether the // pointed-to memory still exists or not. void CheckStatuses() const { for_each(others.begin(), others.end(), [] (weak_ptr<Controller> wp) { try { auto p = wp.lock(); wcout << L"Status of " << p->Num << " = " << p->Status << endl; } catch (bad_weak_ptr b) { wcout << L"Null object" << endl; } }); } }; void RunTest() { vector<shared_ptr<Controller>> v; v.push_back(shared_ptr<Controller>(new Controller(0))); v.push_back(shared_ptr<Controller>(new Controller(1))); v.push_back(shared_ptr<Controller>(new Controller(2))); v.push_back(shared_ptr<Controller>(new Controller(3))); v.push_back(shared_ptr<Controller>(new Controller(4))); // Each controller depends on all others not being deleted. // Give each controller a pointer to all the others. for (int i = 0 ; i < v.size(); ++i) { for_each(v.begin(), v.end(), [v,i] (shared_ptr<Controller> p) { if(p->Num != i) { v[i]->others.push_back(weak_ptr<Controller>(p)); wcout << L"push_back to v[" << i << "]: " << p->Num << endl; } }); } for_each(v.begin(), v.end(), [](shared_ptr<Controller>& p) { wcout << L"use_count = " << p.use_count() << endl; p->CheckStatuses(); }); } int main() { RunTest(); wcout << L"Press any key" << endl; char ch; cin.getline(&ch, 1); }
創建 Controller0
創建 Controller1
創建 Controller2
創建 Controller3
創建 Controller4
push_back 到 v[0]: 1
push_back 到 v[0]: 2
push_back 到 v[0]: 3
push_back 到 v[0]: 4
push_back 到 v[1]: 0
push_back 到 v[1]: 2
push_back 到 v[1]: 3
push_back 到 v[1]: 4
push_back 到 v[2]: 0
push_back 到 v[2]: 1
push_back 到 v[2]: 3
push_back 到 v[2]: 4
push_back 到 v[3]: 0
push_back 到 v[3]: 1
push_back 到 v[3]: 2
push_back 到 v[3]: 4
push_back 到 v[4]: 0
push_back 到 v[4]: 1
push_back 到 v[4]: 2
push_back 到 v[4]: 3
use_count = 1
1 的狀態 = 打開
2 的狀態 = 打開
3 的狀態 = 打開
4 的狀態 = 打開
use_count = 1
0 的狀態 = 打開
2 的狀態 = 打開
3 的狀態 = 打開
4 的狀態 = 打開
use_count = 1
0 的狀態 = 打開
1 的狀態 = 打開
3 的狀態 = 打開
4 的狀態 = 打開
use_count = 1
0 的狀態 = 打開
1 的狀態 = 打開
2 的狀態 = 打開
4 的狀態 = 打開
use_count = 1
0 的狀態 = 打開
1 的狀態 = 打開
2 的狀態 = 打開
3 的狀態 = 打開
正在銷毀 Controller0
正在銷毀 Controller1
正在銷毀 Controller2
正在銷毀 Controller3
正在銷毀 Controller4
按任意鍵 作為練習,將向量 others
修改為 vector<shared_ptr<Controller>>
,然後在輸出中,註意當 TestRun
返回時,未調用析構函數。
c++ 智能指針(轉)