C++ 中的智慧指針-基礎
- 2021 年 2 月 18 日
- 筆記
簡介
在現代 C++ 編程中,標準庫包含了智慧指針(Smart pointers)。
智慧指針用來確保程式不會出現記憶體和資源的泄漏,並且是”異常安全”(exception-safe)的。
智慧指針的使用
智慧指針定義在頭文件 memory
里的命名空間 std
中。它對於資源獲取即初始化(RAII, Resource Acquisition Is Initialization) 編程理念至關重要。該理念的目的是保證對象初始化的時候也是資源獲取的時候,從而使對象的所有資源在單行程式碼中創建。
實踐中,RAII 的主要原則就是把任何在堆上分配的資源(比如動態分配的記憶體或者系統對象的處理)的所有權提供給在棧上分配的對象(其析構函數包含釋放資源及相關清理的程式碼)。
大多數時候,當你初始化一個原始指針或者資源句柄使其指向實際的資源時,立即將其傳給智慧指針。
在現代 C++ 中,原始指針只用於包含在局部作用域,循環或者工具函數的小塊程式碼中(對性能有要求,並且對資源的所有權也不容易混淆)。
原始指針和智慧指針的聲明比較如下:
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.
如上所示,智慧指針是一個在棧上聲明的類模板,並由指向分配在堆上的對象的原始指針初始化。當智慧指針初始化後,它就擁有了原始指針的所有權。這意味著智慧指針需要負責原始指針指向的記憶體釋放。智慧指針的析構函數包含了 delete
的調用,並且由於智慧指針是在棧上聲明的,其析構函數會在智慧指針對象離開作用域時被調用,即使在棧中發生了異常。
通過使用指針運算符(->
和 *
)訪問被封裝的指針,智慧指針類重載了這些運算符以返回被封裝的原始指針。
C++ 智慧指針的理念類似於在 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
創建的對象的指針傳給智慧指針的構造函數。 - 使用重載的操作符
->
和*
來訪問對象。 - 讓智慧指針來
delete
對象。
智慧指針在設計上兼顧了記憶體和性能的高效性。例如,unique_ptr
唯一的數據成員是被封裝的原始指針,這意味著 unique_ptr
具有原始指針同樣地大小,4 位元組或者 8 位元組。通過智慧指針重載的操作符 ->
和 *
來訪問並不比直接使用原始指針來訪問慢多少。
智慧指針有其自己的成員函數,通過 .
來訪問。例如,一些 C++ 標準庫的智慧指針有用於重置的成員函數來釋放對原始指針的所有權。這可以用於在智慧指針超出作用域前釋放智慧指針管理的記憶體,看下面的示例:
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...
}
智慧指針通常提供了獲取原始指針的方式。 C++ 標準庫中的智慧指針包含了成員函數 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());
}
智慧指針的種類
以下部分總結了在 Windows 環境下不同種類的智慧指針,以及如何使用它們。
C++ 標準庫中的智慧指針
優先使用下列智慧指針來封裝原始指針指向的純舊對象(plain old C++ objects,POCO):
-
unique_ptr
- 對封裝的原始指針是獨佔的
- 默認用於 POCO,除非你明確的知道你需要一個
shared_ptr
- 可以移入新的所有者,但不能拷貝或者共享
- 替代
auto_ptr
,auto_ptr
已作廢 - 對比
boost::scoped_ptr
,unique_ptr
更加小巧和高效 - 長度為一個指針的大小,並且支援右值引用來快速執行 C++ 標準庫容器的插入和遍歷操作
-
shared_ptr
- 引用計數智慧指針
- 當你需要將原始指針分派給多個所有者時使用,例如,當你從容器返回一個指針的拷貝並且想要保留它
- 原始指針不會被
delete
直到所有的shared_ptr
超出作用域或者放棄所有權。 - 長度為兩個指針的大小,一個用於對象,另一個用於包含引用計數的共享控制塊
-
weak_ptr
- 結合
shared_ptr
使用的特殊智慧指針。 weak_ptr
提供了對被一個或者多個shared_ptr
所擁有的對象的訪問,但不參與引用計數。- 如果你想要監測某個對象,不要求其不被釋放,可以使用
weak_ptr
- 在某些情況下,用於解決
shared_ptr
實例間的循環引用。
- 結合
擴展
- 用於 COM 組件的智慧指針
- 用於 POCO對象的ATL智慧指針