走進C++11(三十三)shared_ptr
關注公眾號獲取更多資訊:
shared_ptr實現共享式擁有概念。多個智慧指標可以指向相同物件,該物件和其相關資源會在“最後一個引用被銷燬”時候釋放。從名字share就可以看出了資源可以被多個指標共享,它使用計數機制來表明資源被幾個指標共享。可以通過成員函式use_count()來檢視資源的所有者個數。除了可以通過new來構造,還可以通過傳入auto_ptr, unique_ptr,weak_ptr來構造。當我們呼叫release()時,當前指標會釋放資源所有權,計數減一。當計數等於0時,資源會被釋放。
1. shared_ptr是什麼
首先shared_ptr是一個模板類。它是一種過載了->和*操作符的類,由類來實現對記憶體的管理,確保即使有異常產生,也可以通過智慧指標類的解構函式完成記憶體的釋放。
2. 為什麼要用shared_ptr
shared_ptr並不能使程式變得更快,相反,由於控制塊的引入,會使程式佔用更多的記憶體。相應的引用計數邏輯會增加程式的執行開銷。那麼為什麼要使用shred_ptr呢?
share_ptr,用於生存期控制,能夠確保自動正確的銷燬動態分配的物件,防止記憶體洩露。
每一個shared_ptr的拷貝都指向相同的記憶體。在最後一個shared_ptr析構的時候, 記憶體才會被釋放。確保不會多次釋放或者懸空指標。
歸根到底一句話就是:儘量減少記憶體的錯誤使用。
如果你寫程式靠譜,不會出現記憶體洩漏,重複刪除指標,懸空指標等情況,建議不要使用shared_ptr。
3. shared_ptr是如何實現它的特性的
可以認為每個shared_ptr都有一個關聯的計數器,通常稱其為引用計數(reference count)。無論何時拷貝一個shared_ptr,計數器都會遞增。例如,當用一個shared_ptr初始化另一個shared_ptr,或將它作為引數傳遞給一個函式以及作為函式的返回值時,它所關聯的計數器就會遞增。當給shared_ptr賦予一個新值或是shared_ptr被銷燬(例如一個區域性的shared_ptr離開其作用域)時,計數器就會遞減。一旦一個shared_ptr的計數器變為0,它就會自動釋放自己所管理的物件。
當指向一個物件的最後一個shared_ptr被銷燬時,shared_ptr類會自動銷燬此物件。它是通過另一個特殊的成員函式解構函式(destructor)來完成銷燬工作的。類似於建構函式,每個類都有一個解構函式。就像建構函式控制初始化一樣,解構函式控制此型別的物件銷燬時做什麼操作。shared_ptr的解構函式會遞減它所指向的物件的引用計數。如果引用計數變為0,shared_ptr的解構函式就會銷燬物件,並釋放它佔用的記憶體。
4. shared_ptr 如何建立
可以通過建構函式、賦值函式或者make_shared函式初始化智慧指標。
最安全的分配和使用動態記憶體的方法是呼叫一個名為make_shared的標準庫函式。此函式在動態記憶體中分配一個物件並初始化它,返回指向此物件的shared_ptr。當要用make_shared時,必須指定想要建立的物件的型別,定義方式與模板類相同。在函式名之後跟一個尖括號,在其中給出型別。例如,呼叫make_shared<string>時傳遞的引數必須與string的某個建構函式相匹配。如果不傳遞任何引數,物件就會進行值初始化。
Note:1. shared_ptr的預設能力是管理動態記憶體,但支援自定義的Deleter以實現個性化的資源釋放動作。
2. 如果我們不初始化一個智慧指標,它就會被初始化成一個空指標,接受指標引數的職能指標是explicit的,因此我們不能將一個內建指標隱式轉換為一個智慧指標,必須直接初始化形式來初始化一個智慧指標
shared_ptr<int> p1 = new int(1024);//錯誤:必須使用直接初始化形式
shared_ptr<int> p2(new int(1024));//正確:使用了直接初始化形式
使用shared_ptr注意事項:
不要把一個原生指標給多個shared_ptr管理 |
不要把this指標給shared_ptr |
不要在函式實參裡建立shared_ptr |
不要在函式實參裡建立shared_ptr |
通常用auto定義一個物件來儲存make_shared的結果 |
不要不加思考地把指標替換為shared_ptr來防止記憶體洩漏,shared_ptr並不是萬能的,而且使用它們的話也是需要一定的開銷的 |
環狀的鏈式結構shared_ptr將會導致記憶體洩漏(可以結合weak_ptr來解決) |
共享擁有權的物件一般比限定作用域的物件生存更久,從而將導致更高的平均資源使用時間 |
在多執行緒環境中使用共享指標的代價非常大,這是因為你需要避免關於引用計數的資料競爭 |
共享物件的析構器不會在預期的時間執行 |
不使用相同的內建指標值初始化(或reset)多個智慧指標 |
不delete get()返回的指標 |
不使用get()初始化或reset另一個智慧指標 |
如果使用get()返回的指標,記住當最後一個對應的智慧指標銷燬後,你的指標就變為無效了 |
如果你使用智慧指標管理的資源不是new分配的記憶體,記住傳遞給它一個刪除器 |
shared_ptr的型別轉換不能使用一般的static_cast,這種方式進行的轉換會導致轉換後的指標無法再被shared_ptr物件正確的管理。應該使用專門用於shared_ptr型別轉換的 static_pointer_cast<T>() , const_pointer_cast<T>() 和dynamic_pointer_cast<T>() |
下面我們看一下官網對shared_ptr詳細的描述:
定義於標頭檔案 <memory> | |
template< class T > class shared_ptr; | (C++11 起) |
std::shared_ptr是通過指標保持物件共享所有權的智慧指標。多個 shared_ptr 物件可佔有同一物件。下列情況之一出現時銷燬物件並解分配其記憶體:
-
最後剩下的佔有物件的 shared_ptr 被銷燬;
-
最後剩下的佔有物件的 shared_ptr 被通過 operator= 或 reset() 賦值為另一指標。
用 delete 表示式或在構造期間提供給 shared_ptr 的定製刪除器銷燬物件。
shared_ptr 能在儲存指向一個物件的指標時共享另一物件的所有權。此特效能用於在佔有其所屬物件時,指向成員物件。儲存的指標為 get() 、解引用及比較運算子所訪問。被管理指標是在 use_count 抵達零時傳遞給刪除器者。
shared_ptr 亦可不佔有物件,該情況下稱它為空 (empty) (空 shared_ptr 可擁有非空儲存指標,若以別名使用建構函式建立它)。
shared_ptr 的所有特化滿足可複製構造 (CopyConstructible) 、可複製賦值 (CopyAssignable) 和可小於比較 (LessThanComparable) 的要求並可按語境轉換為 bool 。
多個執行緒能在 shared_ptr 的不同例項上呼叫所有成員函式(包含複製建構函式與複製賦值)而不附加同步,即使這些例項是副本,且共享同一物件的所有權。若多個執行執行緒訪問同一 shared_ptr 而不同步,且任一執行緒使用 shared_ptr 的非 const 成員函式,則將出現資料競爭;原子函式的 shared_ptr 特化能用於避免資料競爭。
成員型別
成員型別 | 定義 | ||||
element_type |
| ||||
weak_type (C++17 起) | std::weak_ptr<T> |
成員函式
(建構函式) | 構造新的 shared_ptr (公開成員函式) |
(解構函式) | 如果沒有更多 shared_ptr 指向持有的物件,則析構物件 (公開成員函式) |
operator= | 對 shared_ptr 賦值 (公開成員函式) |
修改器 | |
reset | 替換所管理的物件 (公開成員函式) |
swap | 交換所管理的物件 (公開成員函式) |
觀察器 | |
get | 返回儲存的指標 (公開成員函式) |
operator*operator-> | 解引用儲存的指標 (公開成員函式) |
operator[](C++17) | 提供到被儲存陣列的帶下標訪問 (公開成員函式) |
use_count | 返回 shared_ptr 所指物件的引用計數 (公開成員函式) |
unique(C++20 前) | 檢查所管理物件是否僅由當前 shared_ptr 的例項管理 (公開成員函式) |
operator bool | 檢查是否有關聯的管理物件 (公開成員函式) |
owner_before | 提供基於擁有者的共享指標排序 (公開成員函式) |
非成員函式
make_sharedmake_shared_for_overwrite(C++20) | 建立管理一個新物件的共享指標 (函式模板) |
allocate_sharedallocate_shared_for_overwrite(C++20) | 建立管理一個用分配器分配的新物件的共享指標 (函式模板) |
static_pointer_castdynamic_pointer_castconst_pointer_castreinterpret_pointer_cast(C++17) | 應用 static_cast、dynamic_cast、const_cast 或 reinterpret_cast 到被儲存指標 (函式模板) |
get_deleter | 返回指定型別中的刪除器,若其擁有 (函式模板) |
operator==operator!=operator<operator<=operator>operator>=operator<=> (C++20 中移除)(C++20 中移除)(C++20 中移除)(C++20 中移除)(C++20 中移除)(C++20) | 與另一個 shared_ptr 或 nullptr 進行比較 (函式模板) |
operator<< | 將儲存的指標的值輸出到輸出流 (函式模板) |
std::swap(std::shared_ptr)(C++11) | 特化 std::swap 演算法 (函式模板) |
std::atomic_is_lock_free(std::shared_ptr)std::atomic_load(std::shared_ptr)std::atomic_load_explicit(std::shared_ptr)std::atomic_store(std::shared_ptr)std::atomic_store_explicit(std::shared_ptr)std::atomic_exchange(std::shared_ptr)std::atomic_exchange_explicit(std::shared_ptr)std::atomic_compare_exchange_weak(std::shared_ptr)std::atomic_compare_exchange_strong(std::shared_ptr)std::atomic_compare_exchange_weak_explicit(std::shared_ptr)std::atomic_compare_exchange_strong_explicit(std::shared_ptr) (C++20 中棄用) | 特化的原子操作 (函式模板) |
輔助類
std::hash<std::shared_ptr> (C++11) | std::shared_ptr 的雜湊支援 (類模板特化) |
std::atomic<std::shared_ptr>(C++20) | 原子共享指標 (類模板特化) |
注意
只能通過複製構造或複製賦值其值給另一 shared_ptr ,將物件所有權與另一 shared_ptr 共享。用另一 shared_ptr 所佔有的底層指標建立新的 shared_ptr 導致未定義行為。
std::shared_ptr 可以用於不完整型別 T 。然而,引數為裸指標的建構函式( template<class Y> shared_ptr(Y*) )和 template<class Y> void reset(Y*) 成員函式只可以用指向完整型別的指標呼叫(注意 std::unique_ptr 可以從指向不完整型別的裸指標構造)。
實現說明
在典型的實現中, std::shared_ptr 只保有二個指標:
-
get() 所返回的指標
-
指向控制塊的指標
控制塊是一個動態分配的物件,其中包含:
-
指向被管理物件的指標或被管理物件本身
-
刪除器(型別擦除)
-
分配器(型別擦除)
-
佔有被管理物件的 shared_ptr 的數量
-
涉及被管理物件的 weak_ptr 的數量
以呼叫 std::make_shared 或 std::allocate_shared 建立 shared_ptr 時,以單次分配建立控制塊和被管理物件。被管理物件在控制塊的資料成員中原位構造。通過 shared_ptr 建構函式之一建立 shared_ptr 時,被管理物件和控制塊必須分離分配。此情況中,控制塊儲存指向被管理物件的指標。
shared_ptr 持有的指標是通過 get() 返回的;而控制塊所持有的指標/物件則是最終引用計數歸零時會被刪除的那個。兩者並不一定相等。
shared_ptr 的解構函式會將控制塊中的 shared_ptr 計數器減一,如果減至零,控制塊就會呼叫被管理物件的解構函式。但控制塊本身直到std::weak_ptr計數器同樣歸零時才會釋放。
既存實現中,若有共享指標指向同一控制塊,則自增弱指標計數 ([1],[2]) 。
為滿足執行緒安全要求,引用計數器典型地用等價於用std::memory_order_relaxed的std::atomic::fetch_add自增(自減要求更強的順序,以安全銷燬控制塊)。
舉個小例子:
#include <iostream>
#include <memory>
#include <thread>
#include <chrono>
#include <mutex>
struct Base
{
Base() { std::cout << " Base::Base()\n"; }
// 注意:此處非虛解構函式 OK
~Base() { std::cout << " Base::~Base()\n"; }
};
struct Derived: public Base
{
Derived() { std::cout << " Derived::Derived()\n"; }
~Derived() { std::cout << " Derived::~Derived()\n"; }
};
void thr(std::shared_ptr<Base> p)
{
std::this_thread::sleep_for(std::chrono::seconds(1));
std::shared_ptr<Base> lp = p; // 執行緒安全,雖然自增共享的 use_count
{
static std::mutex io_mutex;
std::lock_guard<std::mutex> lk(io_mutex);
std::cout << "local pointer in a thread:\n"
<< " lp.get() = " << lp.get()
<< ", lp.use_count() = " << lp.use_count() << '\n';
}
}
int main()
{
std::shared_ptr<Base> p = std::make_shared<Derived>();
std::cout << "Created a shared Derived (as a pointer to Base)\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
std::thread t1(thr, p), t2(thr, p), t3(thr, p);
p.reset(); // 從 main 釋放所有權
std::cout << "Shared ownership between 3 threads and released\n"
<< "ownership from main:\n"
<< " p.get() = " << p.get()
<< ", p.use_count() = " << p.use_count() << '\n';
t1.join(); t2.join(); t3.join();
std::cout << "All threads completed, the last one deleted Derived\n";
}
可能的輸出:
Base::Base()
Derived::Derived()
Created a shared Derived (as a pointer to Base)
p.get() = 0x2299b30, p.use_count() = 1
Shared ownership between 3 threads and released
ownership from main:
p.get() = 0, p.use_count() = 0
local pointer in a thread:
lp.get() = 0x2299b30, lp.use_count() = 5
local pointer in a thread:
lp.get() = 0x2299b30, lp.use_count() = 3
local pointer in a thread:
lp.get() = 0x2299b30, lp.use_count() = 2
Derived::~Derived()
Base::~Base()
All threads completed, the last one deleted Derived