什麼是智慧指標?為什麼要用智慧指標?

Mr.Trans?發表於2019-03-29

什麼是智慧指標?為什麼要用智慧指標?如何打破迴圈引用的問題?對於資源管理有什麼作用?

看到這些問題,心裡就發毛。什麼是智慧指標啊?為什麼要用智慧指標啊?迴圈引用又是什麼鬼?實現?我❌...

首先我們來看一下第一個問題,什麼是智慧指標?

常見的智慧指標有幾種,一種是共享指標shared_ptr,一種是獨享指標unique_ptr,一種是弱指標weak_ptr,一種是很久沒用過的auto_ptr(被unique_ptr替代了)。
智慧指標也是指標,它也屬於指標的範疇,但是它比一般的指標智慧,說明了是智慧指標,肯定就智慧一點。就像現在的智慧家居跟普通家居的區別。那麼它到底智慧到什麼程度呢?我們知道,動態記憶體,也就是我們平常在堆中申請記憶體的時候,要是我們用了指標,如下

T *p = new T();
delete p;
複製程式碼

就需要進行管理,例如delete,delete和new是一對操作,delete[]和new[]他兩都是一對cp出現,來虐程式設計師這群單身汪?。new這樣的操作就是為物件分配空間並返回一個指向該物件的指標。所以返回了一個指標,那麼我們就需要管理這個指標,delete就是接受一個動態物件的指標,銷燬該物件(呼叫解構函式,除了內建型別不做處理),並釋放與之關聯的記憶體。
你讓一個單身?去管理?這不鬧著玩嗎?<-_<-! 在動態記憶體的使用,比較容易出現問題,一旦忘記了delete或者在不正確的時間做了不正確的事情,或者在不正確的時間做了正確的事情,那也是不正確,或者在正確的時間做了不正確的事情,就容易發生記憶體洩漏。哪能怎麼辦?
所以 標準庫(記住是標準庫提供的),定義在標頭檔案<memory>,提供了智慧指標,這為了建設和諧美好社會,貢獻了不少。
智慧指標行為類似於常規的指標,這裡只是類似,因為智慧指標會負責自動釋放所指向的物件。(其實就是讓cp滾遠點???)
而且指標並不能指出誰擁有了物件,不知道誰擁有所有權,但是智慧指標卻擁有所有權。

一個人的獨享,感覺擁有了全世界(unique_ptr獨享所有權)

    unique_ptr<string> p1(new string("hi,world")); // 必須採用直接初始化的形式初始化
    unique_ptr<string> p2(p1); // ❌ 不支援拷貝
    unique_ptr<string> p3;
    p3 = p2; // ❌ 不支援賦值
複製程式碼

上面的程式碼,是不是讓unique_ptr感覺很適然,我的東西是我的,你不能留副本,僅此一件。他表示的是互斥所有權。一般的指標都支援拷貝賦值操作,但是這裡他就把拷貝建構函式和拷貝賦值運算子都delete了,根本不讓你拷貝。

  • 一個unique_ptr“擁有”一個物件(它所指向的),某一個時刻,只能有一個unique_ptr指向一個給定的物件。當unique_ptr被銷燬,所指向的物件也被銷燬。
  • unique_ptr不能拷貝,不能賦值,可以移動(p.release())
unique_ptr<string> p1(new string("hi"));
unique_ptr<string> p2(p1.release()); // 將p1置為空,返回指標
cout << *p2 << endl;
unique_ptr<string> p3(new string("hello,world"));
p2.reset(p3.release()); // reset釋放了p2原來指向的記憶體 然後令p2指向p3所指向的物件,然後release()將p3置為空
cout << *p3 << endl; // 輸出的都是hi
cout << *p1 << endl; // p1已經被釋放了,沒有了
複製程式碼

那麼他那麼多限制,在標準庫的實現又是怎樣的呢?來看看下面這一段

template<typename T,typename D = default_delete<T> > // default_delete是一個無狀態類
class unique_ptr{
    public:
        using pointer = ptr;
        using element_type = T;
        using deleter_type = D;
        constexpr unique_ptr() noexcept;
        constexpr unique_ptr(nullptr_t) noexcept:unique_ptr(){} // 空指標型別
        explicit unique_ptr(pointer p) noexcept; // from pointer
        unique_ptr(pointer p,typename conditional<is_reference<D>::value,D,const D&> del) noexcept; // lvalue
        unique_ptr(pointer p,typename remove_reference<D>::type&& del) noexcept; // rvalue
        unique_ptr(unique_ptr&& x) noexcept;// 右值 移動建構函式
        template<class U,class E>
            unique_ptr(unique_ptr<U,E>&& x)noexcept; // 特例化
        template<class U>
            unique_ptr(auto_ptr<U>&& x)noexcept; // 不丟擲異常
        Unique_ptr(const unique_ptr&) = delete; // 不允許拷貝

        unique_ptr& operator=(unique_ptr&& x) noexcept; // 移動賦值運算
        unique_ptr& operator=(nullptr_t) noexcept; // 空指標型別
        template<class U,class E>
            unique_ptr& operator=(unique_ptr<U,E>&& x)noexcept; // 強制型別轉換
        unique_ptr& operator=(const unique_ptr&) = delete; // 不允許賦值
};
複製程式碼

為了不丟擲異常,都設定了noexpect,以上包括了右值引用操作,左值引用操作,以及某一建構函式特例化操作。基本囊括而且也解釋了為什麼不拷貝不賦值。
release()和reset() 這兩個函式都是將指標的所有權從一個(非const)unique_ptr轉移給另一個unique_ptr。
reset()還能好一點,可以釋放記憶體,但是release()就不行了,release()必須有 接盤俠,接了要麼可以自動負責釋放,要麼負責手動釋放。
接下來我們看看這兩個的實現方式

void reset(pointer p = pointer()) noexcept;
// 這裡有一個預設值
pointer release() noexcept;
// 這裡返回一個值
複製程式碼

release()是返回一個pointer,所以說它需要一個接盤俠。

  • unique_ptr 儲存一個指標,當他自身被銷燬時(例如執行緒控制流離開unique_ptr的作用域),使用關聯的釋放器(deleter)釋放所指向的物件
    釋放器又是什麼呢?當一個unique_ptr被銷燬,就會呼叫其自己的釋放器銷燬所擁有的物件。
deleter_type& get_deleter() noexcept;
const deleter_type& get_deleter() const noexcept;
複製程式碼
  1. 區域性變數的釋放器應該啥也不幹
  2. 記憶體池應該將物件歸還給記憶體池,是否銷燬它依賴於記憶體池如何定義。
  3. 預設呼叫delete釋放它所指向的物件
    管理釋放器又分為在執行時繫結和在編譯時繫結,這兩個區別適用於區別shared_ptr和unique_ptr的,下面講完shared_ptr會統一講解,現在只要記住,unique_ptr管理釋放器時編譯時繫結的。
    那怎麼傳遞釋放器呢?我們來看一個?
#include <memory>
#include <iostream>
#include <string>
using namespace std;

class Role{
    public:
        Role(const string &crole):role(crole){
            cout << role << endl;
        }
        ~Role(){
            cout << "delete" << endl;
        }
        void delRole(){
            cout << "delete Role outside" << endl;
        }
    private:
        string role;
};

void outdelRole(Role *r){
    r->delRole();
}

int main(){
    unique_ptr<Role,decltype(outdelRole)*> p1(new Role("trans"),outdelRole);
    return 0;
}
複製程式碼

輸出trans delete Role outside
這個?,充分說明了,我們可以過載釋放器,如果是函式的釋放器,那麼他的引數型別必須是一個objT型別的指標,這樣才有刪除的意義。decltype是一般用來指明型別的

unique_ptr<objT,delT>p(new objT,fcn); // fcn是delT型別物件
複製程式碼

這樣你想怎麼刪,刪什麼就由你自個兒來定了。
也可以這樣做

#include <iostream>
#include <memory>
using namespace std;
class state_deleter {  // a deleter class with state
  int count_;
public:
  state_deleter() : count_(0) {}
  template <class T>
  void operator()(T* p) {
    cout << "[deleted #" << ++count_ << "]\n";
    delete p;
  }
};

state_deleter del;
unique_ptr<int,state_deleter> alpha (new int);
unique_ptr<int,state_deleter> beta (new int,alpha.get_deleter());

// gamma and delta share the deleter "del" (deleter type is a reference!):
unique_ptr<int,state_deleter&> gamma (new int,del);
unique_ptr<int,state_deleter&> delta (new int,gamma.get_deleter());
複製程式碼

再來看一段比較陷阱的程式碼

unique_ptr<string> p1;
cout << *p1 << endl;
複製程式碼

這段程式碼代表p1是一個空指標,那這個空指標,沒有指向一個物件,那下面這一段呢?

unique_ptr<string> p1();
cout << *p1 << endl;
複製程式碼

輸出的是1,為什麼呢?因為unique_ptr<string> p1()宣告一個無參函式p1,返回的型別是unique_ptr型別的指標,所以要是*p1,那隻能是1,他是一個函式體

用途

  • 為動態分配的記憶體提供異常安全
    unique_ptr可以理解為一個簡單的指標(指向一個物件)或一對指標(包含釋放器deleter的情況)
  • 將動態分配記憶體的所有權傳遞給函式
  • 從函式返回動態分配的記憶體
  • 從容器中儲存指標
    ⚠️這裡有一個get()的用法
pointer get() const noexcept;
複製程式碼

get()是託管一個物件的指標或者空指標

unique_ptr<string> p1(new string("hello world"));
string *pstr = p1.get();
cout << *pstr << endl;
複製程式碼

他與release()不同,它只是託管,get並是將pstr指向了p1指向的物件,但是並沒有釋放p1的記憶體,pstr並沒有獲取到這個智慧指標的所有權,只是得到了它的物件。p1還是需要在某個時刻刪除託管資料pstr。
再來看一下解引用運算子

typename add_lvalue_reference<element_type>::type operator*() const;
複製程式碼

作用支援指標操作唄

unique_ptr<string> p1(new string("hello world"));
cout << *p1 << endl;
複製程式碼

再看看->運算子

pointer operator->()const noexcept;
複製程式碼

支援指標行為的操作

  unique_ptr<C> foo (new C);
  unique_ptr<C> bar;

  foo->a = 10;
  foo->b = 20;

  bar = std::move(foo); // 支援右值移動操作 foo就釋放了
複製程式碼

那好,我們知道整個unique_ptr都會支援指標的行為,那我們看看它的特例化版本。什麼是特例化?就是對於特別的?進行特別的處理。不同的版本

template<class T,class D> class unique_ptr<T[],D>;
複製程式碼
// 用於內建函式
unique_ptr<int[]> make_sequence(int n){
    unique_ptr<int[]> p{new int[n]};
    for(int i = 0;i<n;++i)
        p[i] = i;
    return p; // 返回區域性物件
}
複製程式碼

這裡當然要新增加獨一[]運算子的作用,也就是過載 []運算子。

element_type& operator[](size_t i)const;
複製程式碼

不同擔心匹配問題,我們提供特例化版本只是幫編譯器做了匹配的工作而已。
那交換指標?交換也是移動操作呀!

template <class T,class D>
void(unique_ptr<T,D>& x,unique_ptr<T,D>& y)noexpect;
複製程式碼

交換兩方的所有權,你要我的,我要你的。當然這是非成員函式,也有成員函式的寫法

void swap(unique_ptr& x) noexcept;
複製程式碼

就是a.swap(b)醬紫。

共享物件?,你的物件我共享✨o✨(shared_ptr共享所有權)

既然講完了unique_ptr,那我們就來講講這個讓社會更美好的shared_ptr,共享指標。
先來看看怎麼用

shared_ptr<string> p1; 
shared_ptr<list<int> > p2;
複製程式碼

通過預設初始化,p1和p2都是空指標。當然這兩個操作,都沒有分配和使用動態記憶體。要怎麼做呢?我們嘗試這樣。

shared_ptr<string> p1(new string("hehehe"));
cout << *p1 << endl;
複製程式碼

也可以試一下這樣

shared_ptr<int> clone(int p){
    return shared_ptr<int>(new int(p));
}
複製程式碼

也可以管理內建指標inum

int *inum = new int(42);
shared_ptr<int> p2(inum);
複製程式碼

停?停?停?,先說明白,shared_ptr共享指標到底是什麼?
shared_ptr表示共享所有權,和unique_ptr指標不同,shared_ptr可以共享一個物件。當兩段程式碼需要訪問同一個資料,但兩者都沒有獨享所有權(負責銷燬物件)時,可以使用shared_ptr。shared_ptr是一種計數指標,當計數(use_count)變為0時釋放所指向的物件。
可以理解為包含兩個指標的結構,一個指標指向物件,另一個指標指向計數器(use_count)。
而僅僅是因為當計數變為0才會銷燬所指向的物件,它的釋放器(deleter)與unique_ptr就不一樣,是一個非成員函式。但是是一個可呼叫物件,可呼叫物件後面我會專門去講,但是在這裡就要明白,shared_ptr的是釋放器是 執行時繫結的,而不是 編譯時就繫結的。而unique_ptr就是編譯時繫結的釋放器。預設的釋放器是delete,這個卻沒有變。(呼叫物件的解構函式並釋放自由儲存空間)
它的重點就在於使用計數上,那這個計數又是怎麼定義的呢?來看一段程式碼。

shared_ptr<int> p3 = make_shared<int>(42);
cout << p3.use_count() << endl;
複製程式碼

看吧,這裡的use_count()就是用來計數的,現在是1,就是這個物件引用了一次。

    shared_ptr<int> p3 = make_shared<int>(42);
    auto r = p3;
    cout << p3.use_count() << endl;
複製程式碼

這裡就是2了,這裡會怎樣,遞增p3的引用計數,那r呢?r的計數是多少?r是2啊,這裡就是說這個r也指向p3的物件了,那麼這個計數器肯定是一樣的。要是r原來有指向的物件呢?那原來r的指向的物件的計數器也要遞減,也不影響其他的指標。
所以其實區別就是,這些共享所有權的指標,都沒有權利把物件殺死,他把殺物件的事情外包了出去。(不忍心啊!?)。
所以,這麼看來,因為有一個計數器,所以我們可以說,shared_ptr自動銷燬所管理的物件。也可以說,shared_ptr自動釋放相關聯的記憶體。
可以看一下這段程式碼,來看看動態記憶體中的使用

#include <iostream>
#include <memory>
#include <string>
#include <initializer_list>
#include <vector>

using namespace std;

class StrBlob{
    public:
        typedef vector<string>::size_type size_type;
        StrBlob():data(make_shared<vector<string> >()){}
        StrBlob(initializer_list<string> il):data(make_shared<vector<string> >(il)){} // 使用引數列表初始化vector
        size_type size() const { return data->size();}
        bool empty() const { return data->empty();}
        void push_back(const string &t){return data->push_back(t);}
        void pop_back();
        string &front();
        string &back();
    private:
        shared_ptr<vector<string> > data; // 共享同一個資料?
        void check(size_type i,const string &msg) const;
};
複製程式碼

當我們拷貝,賦值或銷燬一個StrBlob物件的時候,這個shared_ptr的資料成員將會被拷貝、賦值和銷燬。那麼每一次都是安全的操作,自動釋放。因為計數器,所以安全。
所以其實也不復雜,就是希望我們可以用shared_ptr進行管理動態記憶體的資源。這裡我待會也會著重講(RAII)
ok,看完了在動態記憶體的資源管理,那我們熟知的動態記憶體是怎樣的?是那對cp,就是new和delete。其實shared_ptr和new也可以一起用。

shared_ptr<double> p1; // shared_ptr 可以指向一個double
shared_ptr<int> p2(new int(42)); // p2指向一個值42的int 直接初始化形式
複製程式碼

我們看建構函式

    template<typename U>
    class shared_ptr{
        public:
            using element_type = U;
            constexpr shared_ptr() noexcept;
            constexpr shared_ptr(nullptr_t):shared_ptr(){} // 空物件
            template <class U> explicit shared_ptr(U* p); // 顯式構造 不存在隱式轉換
            template <class U,class D> shared_ptr(U* p,D del); // 新增釋放器
            template <class D> shared_ptr(nullptr_t p,D del); // 空指標的釋放器
            template <class U,class D, class Alloc> shared_ptr(U* p,D del,Alloc alloc); // 分配?
            template <class D,class Alloc> shared_ptr(nullptr_t p,D del,Alloc alloc);
            shared_ptr(const shared_ptr& x) noexcept;
            template<class U> shared_ptr(const shared_ptr<U>& x)noexcept;
            template<class U> explicit shared_ptr(const weak_ptr<U>& x);
            shared_ptr(shared_ptr&& x)(shared_ptr<U>&& x)noexcept; // 右值移動
            template <class U> shared_ptr(auto_ptr<U>&& x);
            template <class U,class D> shared_ptr(unique_ptr<U,D>&& x);// 獲得獨享指標的所有權
            template <class U> shared_ptr(const shared_ptr<U>& x,element_type* p)noexcept;
    };
複製程式碼

在建構函式中,接受指標引數的智慧指標建構函式是explicit,就是顯式構造,而不是隱式轉換。

shared_ptr<int> clone(int p){
    return shared_ptr<int>(new int(p));
}
複製程式碼

在primer中,建議 不要混合使用普通指標和智慧指標,怎麼才算是混合呢?我們來看一下它給的?。

void process(shared_ptr<int> ptr){
    // 使用ptr
}// ptr離開作用域,被銷燬
複製程式碼

在這個?中,ptr是值傳遞,大家都知道,值傳遞會增加拷貝,構造等成本,所以ptr計數值至少為2,很公道,當process結束時,計數值不會變為0。所以區域性變數ptr被銷燬,ptr指向的記憶體也不會釋放。(所以說使用引用會減少增加引用計數)

void process(shared_ptr<int>& ptr){
    cout << ptr.use_count() << endl;
    cout << *ptr << endl;
}
複製程式碼

當我們使用值傳遞的時候,引用計數至少為2,但是使用引用傳遞,引用計數就不會遞增

    shared_ptr<int> p3 = make_shared<int>(42);
    cout << p3.use_count() << endl;
    // auto r = p3;
    // cout << r.use_count() << endl;
    process(p3);
    cout << p3.use_count() << endl;
複製程式碼

使用引用計數,輸出始終如一。
看來這個?只能做引用和值傳遞的,好像和混合使用普通指標和智慧指標沒啥搭邊啊!

    int *x(new int(9));
    process(shared_ptr<int>(x));
    int j = *x;
    cout << j << endl;
複製程式碼

上面的?我們使用的是值傳遞。嗯。這個?說明什麼呢?可能不是很懂shared_ptr<int>(x)這種騷操作,我們來看一下這樣會不會懂了一點

shared_ptr<int> ptr = shared_ptr<int>(new int(10));
複製程式碼

懂了吧。

shared_ptr<T> p(q);
複製程式碼

q是內建指標,p管理這個內建指標所指向的物件。q必須指向new分配的記憶體且能夠轉換為T*型別。
所以上上面的例子說明了,這兩個混合著用,臨時的shared_ptr會被銷燬,那所指向的記憶體也會被釋放。所以x估計還指向那個記憶體,但是,x已經不知不覺中變成空懸指標了。
其實當講一個shared_ptr繫結到一個普通指標時,我們就將記憶體的管理責任交給了這位不知名的shared_ptr。所以,我們就不能或者不應該再使用內建指標訪問shared_ptr所指向的記憶體。
primer也建議 不要使用get初始化另一個智慧指標或為智慧指標賦值。
get()函式上面也有簡略的介紹,它的作用是,它返回一個內建指標,指向智慧指標管理的物件。它的設計是為了在需要向不能使用智慧指標的程式碼傳遞一個內建指標。什麼意思?它只是一個託管指標。來看看這段程式碼

shared_ptr<int> p(new int(42));
int *q = p.get();
{
    // 兩個獨立的shared_ptr指向相同的記憶體
    shared_ptr<int>(q);
    // 離開作用域就會釋放
}
int foo = *q; // 最後未定義
複製程式碼

所以這裡解釋了不能用get()這樣的初始化另一個智慧指標,get()畢竟是託管,給你的都是已經有的,託管而已,給了你,你也是指向相同的記憶體。
當然,shared_ptr也可以使用reset操作

    string *inum = new string("hhh");
    shared_ptr<string> p5 = make_shared<string>("hi");
    p5.reset(inum);
複製程式碼

但是他只能用於內建指標傳遞。
還能傳遞釋放器給shared_ptr p5.reset(inum,d);
那為什麼shared_ptr沒有release成員? 沒有所有權唄。 講了那麼多,make_shared一直都像是被忽略了。

template <class T,class ... Args>
    shared_ptr<T> make_shared(Args&&... args);
複製程式碼

這是它的原始碼,他的用途就是製作shared_ptr,返回型別為shared_ptr<T>的物件,該物件擁有並儲存指向它的指標(引用次數為1)。
看看怎麼使用

auto baz =make_shared<pair<int,int> > (30,40);
... baz->first .. << baz->second
複製程式碼

ok,所以,當我們使用shared_ptr初始化的時候,最好最安全就是使用這個標準庫函式,並且使用new肯定還要轉換啊,給予所有權,但是make_shared幫你將分配,安全都做好了,而且給你的就是返回的shared_ptr的型別物件,讓你的指標指向就行了。
推薦使用哦!
shared_ptr,其實就是一個指標,套上了釋放器,套上了計數器,拷貝的時候增加了引用,賦值也增加了引用,相應的也會有遞減了引用計數。我們再來看另外一種情況

struct Node{
    shared_ptr<Node> pPre;
    shared_ptr<Node> pNext;
    int val;
};
void func(){
    shared_ptr<Node> p1(new Node());
    shared_ptr<Node> p2(new Node());
    cout << p1.use_count() << endl;
    cout <<p2.use_count() << endl;
    p1->pNext = p2;
    p2->pPre = p1;
    cout << p1.use_count() << endl;
    cout <<p2.use_count() << endl;
}
複製程式碼

我們看到,p1是2,p2也是2,他們互相拷貝引用啊!要想釋放p2就要先釋放p1,而要想釋放p1,就得釋放p2,這樣就是 迴圈引用了,最後p1和p2指向的記憶體空間永遠都無法釋放掉。
那可咋辦咧,上面介紹的竟然沒有一種能解決,不要慌,不要忙,靜靜在兩旁。
靜靜往下看看。

weak_ptr 讓靜靜繼續靜靜 該走的還是讓你走

上面這個就是一個環,我們怎樣打破這個環,讓記憶體釋放呢?使用weak_ptr。介紹一下weak_ptr,一種不控制所指向物件生存期的智慧指標,指向由一個shared_ptr管理的物件。看來這也是共享所有權的樂趣,眾人幫,不像unique_ptr,一個人孤苦伶仃。
不控制是什麼意思?就是weak_ptr,不影響shared_ptr的引用計數。一旦shared_ptr被銷燬,那麼物件也會被銷燬,即使weak_ptr還指向這個物件,這個物件也會被銷燬。所以說,該走的還是讓你走。
所以它也叫做"弱"共享所有權。
只引用,不計數,但是有沒有,要檢查expired()應運而生。
我們來看一下他的構造以及使用

template <class T> class weak_ptr{
    public:
    constexpr weak_ptr() noexcept;
    weak_ptr(const weak_ptr& x) noexcept;
    template <class U> weak_ptr(const weak_ptr<U>& x) noexcept;
    template <class U> weak_ptr(const shared_ptr<U>& x) noexcept;
}
複製程式碼

所以從建構函式可以看出,這個weak_ptr,可以自己構造,也可以指向share_ptr,而且僅僅是引用。

shared_ptr<int> sp(new int(42));
weak_ptr<int> wp(sp);
cout << wp.use_count << endl;
複製程式碼

那use_count呢?

long int use_count() const noexcept;
複製程式碼

看到了嘛。它並不會改變引用計數。const
那expired是什麼? 它只是檢查use_count()是不是變為0了,為0返回false,否則返回true。

bool expired() const noexcept;
複製程式碼

這是用來檢查一下這個指標所指向的物件是否被銷燬了。
所以這就導致物件可能就不存在,因此我們不能使用weak_ptr直接訪問物件,況且weak_ptr也沒有*這個訪問運算子過載的過程,就需要呼叫別的函式,例如lock

shared_ptr<T> lock() const noexcept;
複製程式碼

lock() 會檢查weak_ptr所指向的物件是否存在,如果存在就返回一個共享物件shared_ptr。

#include <iostream>
#include <memory>

int main () {
  std::shared_ptr<int> sp1,sp2;
  std::weak_ptr<int> wp;
                                       // sharing group:
                                       // --------------
  sp1 = std::make_shared<int> (20);    // sp1
  wp = sp1;                            // sp1, wp

  sp2 = wp.lock();                     // sp1, wp, sp2
  sp1.reset();                         //      wp, sp2

  sp1 = wp.lock();                     // sp1, wp, sp2

  std::cout << "*sp1: " << *sp1 << '\n';
  std::cout << "*sp2: " << *sp2 << '\n';

  return 0;
}
複製程式碼

很清楚,都輸出20。同樣,reset就能置空一個weak_ptr
那麼為什麼,weak_ptr能破環呢?我們繼續來看下面這一段程式碼

struct Node{
    weak_ptr<Node> pPre; // 區別⬅️⬅️⬅️
    weak_ptr<Node> pNext; // 區別⬅️⬅️⬅️
    int val;
    Node(){
        cout << "construct" << endl;
    }
    ~Node(){
        cout << "delete" <<endl;
    }
};
void func(){
    shared_ptr<Node> p1(new Node());
    shared_ptr<Node> p2(new Node());
    cout << p1.use_count() << endl;
    cout << p2.use_count() << endl;
    p1->pNext = p2;
    p2->pPre = p1;
    cout << p1.use_count() << endl;
    cout << p2.use_count() << endl;
}
複製程式碼

這就打破了迴圈引用的環,因為每一個shared_ptr都會將引用計數設為1,那麼每次用都會遞增,所以要是不遞增,用原來的指向的物件不就解決了嘛。改一下結構就完美解決,而且還能呼叫了解構函式。

shared_ptr與unique_ptr釋放器 一動一靜顯神通

講完了weak_ptr,突然感覺,智慧指標的發明確實偉大!單身?迷茫的時候容易犯的錯誤變得不再容易。那麼,每次我們都會發現,這兩個指標,會有一個釋放器。
unique_ptr版本

unique_ptr<T,D> up;
複製程式碼

shared_ptr版本

shared_ptr<T> p(q,d);
複製程式碼

不管大小寫的d都是delete,釋放器。向unique_ptr我們之前介紹過,這是一個確定的刪除器,在編譯時就已經決定了它的型別了。
unique_ptr

template<typename T,typename D = default_delete<T> > // default_delete是一個無狀態類
class unique_ptr{
    public:
        using pointer = ptr;
        using element_type = T;
        using deleter_type = D;
        ...
複製程式碼

那shared_ptr咧

template<typename U>
class shared_ptr{
    public:
        using element_type = U;
        constexpr shared_ptr() noexcept;
        constexpr shared_ptr(nullptr_t):shared_ptr(){} // 空物件
        template <class U> explicit shared_ptr(U* p); // 顯式構造 不存在隱式轉換
        ...
複製程式碼

看到這個template就明白,原來shared_ptr一直沒有固定型別的釋放器,雖然預設是delete,但是也可以使用可呼叫物件,看看下面這個可呼叫物件的例子

#include <iostream>
#include <memory>

int main () {
   auto deleter = [](Node* p){
    cout << "[deleter called]\n"; 
    delete p;
    };
    // shared_ptr<int> foo (new int,deleter);
    // cout << "use_count: " << foo.use_count() << '\n';
    shared_ptr<Node> bar(new Node(),deleter);
  return 0;                        // [deleter called]
}
複製程式碼

所以釋放器,無論是unique_ptr還是shared_ptr都必須儲存為一個指標或一個封裝了指標的類。但我們也可以確定,shared_ptr不是將釋放器直接儲存為一個成員,因為它的型別直到執行時才知道。
因為shared_ptr只有一個模版引數,而unique_ptr有兩個模版引數,所以在這個unique_ptr的工作方式,我們可以看出來,這個釋放器的型別是unique_ptr型別的一部分,所以釋放器可以直接儲存在unique_ptr物件中。
兩個釋放器都是對其儲存的指標呼叫使用者提供提供的釋放器或執行delete
所以,總結一下,通過編譯時繫結釋放器,unique_ptr避免了間接呼叫釋放器的執行時開銷。
通過執行時繫結釋放器,shared_ptr使使用者過載釋放器更加方便。
所以這些都是以物件來管理資源的例子,一個一個shared_ptr,unique_ptr都在以物件的形式管理著資源,防止資源的洩露,動態記憶體再也不用害怕洩漏了。
額,那可呼叫物件又有哪些呢?怎麼用呢?為什麼shared_ptr可以這樣用可呼叫物件呢?
釋出了這篇文章,希望後端大牛們隨便噴,小弟定當改進?。
另: 寫文不易,轉載請標明出處

未完待續...

相關文章