1. 程式人生 > 其它 >走進C++11(三十三)shared_ptr

走進C++11(三十三)shared_ptr

技術標籤:C++11c++c++11智慧指標

關注公眾號獲取更多資訊:

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
T(C++17 前)
std::remove_extent_t<T>(C++17 起)
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() = 1Shared ownership between 3 threads and releasedownership from main:  p.get() = 0, p.use_count() = 0local pointer in a thread:  lp.get() = 0x2299b30, lp.use_count() = 5local pointer in a thread:  lp.get() = 0x2299b30, lp.use_count() = 3local pointer in a thread:  lp.get() = 0x2299b30, lp.use_count() = 2  Derived::~Derived()  Base::~Base()All threads completed, the last one deleted Derived