[c++11]智能指針學(xué)習(xí)筆記

C#辽聊、Java痴脾、pythongo等語言中都有垃圾自動回收機制颤介,在對象失去引用的時候自動回收,而且基本上沒有指針的概念赞赖,而C++語言不一樣滚朵,C++充分信任程序員,讓程序員自己去分配和管理堆內(nèi)存前域,如果管理的不好辕近,就會很容易的發(fā)生內(nèi)存泄漏問題,而C++11增加了智能指針(Smart Pointer)话侄。主要分為shared_ptr亏推、unique_ptrweak_ptr三種学赛,使用時需要引用頭文件<memory>年堆。c++98中還有auto_ptr,基本被淘汰了盏浇,不推薦使用变丧。而c++11shared_ptrweak_ptr都是參考的boost庫中實現(xiàn)的。

本文有很多代碼片段绢掰,如果不喜歡簡書的代碼風(fēng)格痒蓬,請移步我的SegmentFault博客

原始指針

c語言中最常使用的是malloc()函數(shù)分配內(nèi)存滴劲,free()函數(shù)釋放內(nèi)存攻晒,而c++中對應(yīng)的是newdelete關(guān)鍵字班挖。malloc()只是分配了內(nèi)存鲁捏,而new則更進一步,不僅分配了內(nèi)存萧芙,還調(diào)用了構(gòu)造函數(shù)進行初始化给梅。使用示例:

int main()
{
    // malloc返回值是 void*
    int* argC = (int*)malloc(sizeof(int));
    free(argC);

    char*c = (char*)malloc(100);
    free(c);

    char *age = new int(25); //做了兩件事情 1.分配內(nèi)存 2.初始化
    int* height = new int(160);

    delete height;
    delete age;
    
    char* arr = new int[100];
    delete[] arr;
    /*delete數(shù)組需要使用delete[],事實上双揪,c++原始支持的數(shù)據(jù)結(jié)構(gòu)組成的
    數(shù)組不需要[]也可以动羽,但 自定義的數(shù)據(jù)類型組成的數(shù)組必須使用delete[]*/
}

newdelete必須成對出現(xiàn),有時候是不小心忘記了delete渔期,有時候則是很難判斷在這個地方自己是不是該delete运吓,這個和資源的生命周期有關(guān),這個資源是屬于我這個類管理的還是由另外一個類管理的,如果是我管理的羽德,就由我來delete几莽,由別人管理的就由別人delete,我就算析構(gòu)了也不影響該資源的生命周期宅静。例如:

// 情況1: 需要自己delete
const char* getName() {
    char *valueGroup = new char[1000];
    // do something
    return valueGroup;
}
// 情況2: 不需要自己delete
const char* getName2() {
    static char valueGroup[1000];
    // do something
    return valueGroup;
}

只通過函數(shù)簽名來看章蚣,這兩個函數(shù)沒有什么區(qū)別,但是由于實現(xiàn)的不同姨夹,有時候需要自己管理內(nèi)存纤垂,有時候不需要,這個時候就需要看文檔說明了磷账。這就是使用一個"裸"指針不好的地方峭沦。

一點改進是,如果需要自己管理內(nèi)存的話逃糟,最好顯示的將自己的資源傳遞進去吼鱼,這樣的話,就能知道是該資源確實應(yīng)該由自己來管理绰咽。

char *getName(char* v, size_t bufferSize) {
    //do something
    return v;
}

上面還是小問題菇肃,自己小心一點,再仔細(xì)看看文檔取募,還是有機會避免這些情況的琐谤。但是在c++引入異常的概念之后,程序的控制流就發(fā)生了根本性的改變玩敏,在寫了delete的時候還是有可能發(fā)生內(nèi)存泄漏斗忌。如下例:

void badThing(){
    throw 1;// 拋出一個異常
}

void test() {
    char* a = new char[1000];

    badThing();
    // do something
    delete[] a;
}
int main() {
    try {
        test();
    }
    catch (int i){
        cout << "error happened " << i << endl;
    }
}

上面的newdelete是成對出現(xiàn)的,但是程序在中間的時候拋出了異常旺聚,由于沒有立即捕獲织阳,程序從這里退出了,并沒有執(zhí)行到delete砰粹,內(nèi)存泄漏還是發(fā)生了唧躲。

C++中的構(gòu)造函數(shù)和析構(gòu)函數(shù)十分強大,可以使用構(gòu)造和析構(gòu)解決這種問題伸眶,比如:

class SafeIntPointer {
public:
    explicit SafeIntPointer(int v) : m_value(new int(v)) { }
    ~SafeIntPointer() {
        delete m_value;
        cout << "~SafeIntPointer" << endl;
    }
    int get() { return *m_value; }
private:
    int* m_value;
};

void badThing(){
    throw 1;// 拋出一個異常
}

void test() {
    SafeIntPointer a(5);

    badThing();
}
int main() {
    try {
        test();
    }
    catch (int i){
        cout << "error happened " << i << endl;
    }
}

// 結(jié)果
// ~SafeIntPointer
// error happened 1

可以看到惊窖,就算發(fā)生了異常,也能夠保證析構(gòu)函數(shù)成功執(zhí)行厘贼!這里的例子是這個資源只有一個人使用界酒,我不用了就將它釋放掉。但還有種情況嘴秸,一份資源被很多人共同使用毁欣,要等到所有人都不再使用的時候才能釋放掉庇谆,對于這種問題,就需要對上面的SafeIntPointer增加一個引用計數(shù)凭疮,如下:

class SafeIntPointer {
public:
    explicit SafeIntPointer(int v) : m_value(new int(v)), m_used(new int(1)) { }
    ~SafeIntPointer() {
        cout << "~SafeIntPointer" << endl;
        (*m_used) --; //引用計數(shù)減1
        if(*m_used <= 0){
            delete m_used;
            delete m_value;
            cout << "real delete resources" << endl;
        }

    }
    SafeIntPointer(const SafeIntPointer& other) {
        m_used = other.m_used;
        m_value = other.m_value;
        (*m_used)++; //引用計數(shù)加1
    }
    SafeIntPointer& operator= (const SafeIntPointer& other) {
        if (this == &other) // 避免自我賦值!!
           return *this;

        m_used = other.m_used;
        m_value = other.m_value;
        (*m_used)++; //引用計數(shù)加1
        return *this;
    }

    int get() { return *m_value; }
    int getRefCount() {
        return *m_used;
    }

private:
    int* m_used;
    int* m_value;
};

int main() {
    SafeIntPointer a(5);
    cout << "ref count = " << a.getRefCount() << endl;
    SafeIntPointer b = a;
    cout << "ref count = " << a.getRefCount() << endl;
    SafeIntPointer c = b;
    cout << "ref count = " << a.getRefCount() << endl;
}

/*
ref count = 1
ref count = 2
ref count = 3
~SafeIntPointer
~SafeIntPointer
~SafeIntPointer
real delete resources
*/

可以看到每一次賦值饭耳,引用計數(shù)都加一,最后每次析構(gòu)一次后引用計數(shù)減一执解,知道引用計數(shù)為0寞肖,才真正釋放資源。要寫出一個正確的管理資源的包裝類還是蠻難的衰腌,比如上面那個上面例子就不是線程安全的新蟆,只能屬于一個玩具,在實際工程中簡直沒法用右蕊。而到了C++11琼稻,終于提供了一個共享的智能指針解決這個問題。

shared_ptr共享的智能指針

shared_ptr的基本使用

shared_ptr的基本使用很簡單饶囚,看幾個例子就明白了:

#include <iostream>
#include <memory>

class Object {
public:
    Object(int id) : m_id(id) {
        std::cout << "init obj " << std::endl;
    }
    ~Object() {
        std::cout << "bye bye" << m_id << std::endl;
    }
    int id() const {
        return m_id;
    }
private:
      int m_id;
};

// 取個別名 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;

void print(Object* obj) {
    std::cout << "in print(Object* obj) :  " << std::endl;
//    std::cout << "ref count is " << obj.use_count() << std::endl;
}

void print(Object obj) {
    std::cout << "in print(Object obj) :  " << std::endl;
//    std::cout << "ref count is " << obj.use_count() << std::endl;
}

void print(ObjectPtr obj) {
    std::cout << "in print(ObjectPtr obj) :  ";
    std::cout << "ref count is " << obj.use_count() << std::endl;
}

void printRef(const ObjectPtr& obj) {
    std::cout << "in print(const ObjectPtr& obj) :  ";
    std::cout << "ref count is " << obj.use_count() << std::endl;
}

int main() {
    // 創(chuàng)建一個智能指針帕翻,管理的資源就是 new出來的Object(1)
    //ObjectPtr obj=new Object(1); //不能將一個原始指針直接賦值給一個智能指針
    ObjectPtr obj(new Object(1)); // 正確
    std::cout << "ref count is " << obj.use_count() << std::endl; // 1
    ObjectPtr obj2(obj);
    std::cout << "ref count is " << obj.use_count() << std::endl; // 2
    std::cout << "ref count is " << obj2.use_count() << std::endl; // 2 obj和obj2管理的資源是一樣的
    ObjectPtr obj3 = obj2;
    std::cout << "ref count is " << obj.use_count() << std::endl; // 3

    obj2.reset(); // obj2不再管理之前的資源,資源的引用計數(shù)會減1
    // 或者可以寫成 obj2 = nullptr;
    std::cout << "ref count is " << obj.use_count() << std::endl; // 2

    ObjectPtr obj4; //obj4 開始沒有管理資源
    // 將管理的資源 相互交換
    // 交換后 obj3沒有再管理資源萝风, obj4管理obj3之前管理的資源
    // 或者寫成 std::swap(obj3, obj4);
    obj3.swap(obj4);
    std::cout << "ref count is " << obj.use_count() << std::endl; // 還是 2

    // 還可以從智能指針中獲取原始指針
    auto p = obj.get(); // auto = Object*
    // 需要判斷這個obj是否確實管理著資源嘀掸, 可能為 nullptr
    if( p ) {
        std::cout << "p->id() is " << p->id() << std::endl;
        std::cout << "(*p).id() is " << (*p).id() << std::endl;
    }
    // 智能指針也可以像普通指針一樣使用
    // 重載了 operator bool
    if( obj ) {
        std::cout << "obj->id() is " << obj->id() << std::endl; // 重載了operator ->
        std::cout << "(*obj).id() is " << (*obj).id() << std::endl;// 重載了operator *
    }

    // obj.use_count()可以判斷當(dāng)前有多少智能指針在管理資源
    // 如果判斷是不是只有一個人在管理這個資源, 用unique()函數(shù)更加高效
    // unique() 等價于 obj.use_count() == 1
    obj4 = nullptr; // obj4不在管理闹丐,這個時候的引用計數(shù)變成了 1
    std::cout << "ref count is " << obj.use_count() << std::endl; // 1

    if( obj.unique() )
        std::cout << "only one hold ptr "<<  std::endl;
    else
        std::cout << "not noly one hold ptr" << std::endl; //其實也有可能沒有人再管理

    // 將智能指針當(dāng)作參數(shù)傳遞給函數(shù)時
    // 如果是值傳遞横殴, 智能指針發(fā)生一次拷貝被因,
    // 在函數(shù)內(nèi)部時智能指針的引用計數(shù)會 + 1
    // 離開函數(shù)作用域時卿拴, 智能指針會析構(gòu), 引用計數(shù)會 - 1
    print(obj);
    // 如果傳遞的是引用梨与, 對引用計數(shù)沒影響 而且工作量比較小(沒有拷貝)
    // 推薦使用引用方式傳遞堕花, 傳值的方式也有用處,比如多線程時
    printRef(obj);

    // 還可以不傳遞智能指針粥鞋, 傳遞原生類型
    print(*obj); //傳Object類型的時候缘挽,離開函數(shù)的時候參數(shù)obj會發(fā)生一次析構(gòu)
    print(obj.get());

}
/*
init obj
ref count is 1
ref count is 2
ref count is 2
ref count is 3
ref count is 2
ref count is 2
p->id() is 1
(*p).id() is 1
obj->id() is 1
(*obj).id() is 1
ref count is 1
only one hold ptr
in print(ObjectPtr obj) :  ref count is 2
in print(const ObjectPtr& obj) :  ref count is 1
in print(Object obj) :
bye bye1    //這個是在調(diào)用print(Object obj)時,局部變量析構(gòu)時打印的
in print(Object* obj) :
bye bye1 //這個是在資源在沒有人引用的時候呻粹,執(zhí)行析構(gòu)函數(shù)產(chǎn)生的
*/

給shared_ptr指定刪除器

大部分用法都基本上在上面的例子中體現(xiàn)出來了壕曼,當(dāng)沒有人引用這個資源的時候,智能指針的默認(rèn)行為是調(diào)用 delete銷毀這個資源等浊,而我們也可以人為指定這個步驟腮郊,因為有些資源不一定是new出來的,所以不應(yīng)該使用默認(rèn)的delete行為筹燕,還有一個情況是轧飞,在用智能指針管理動態(tài)數(shù)組的時候衅鹿,需要自己指定刪除器函數(shù)。

#include <iostream>
#include <memory>

class Object {
public:
    Object(int id) : m_id(id) {
        std::cout << "init obj " << std::endl;
    }
    ~Object() {
        std::cout << "bye bye" << m_id << std::endl;
    }
    int id() const {
        return m_id;
    }
private:
      int m_id;
};

// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;

void deleterOfObject(Object* obj) {
    if ( obj ) {
        std:: cout << "delete obj " << obj->id() << std::endl;
        delete obj;
    }
}

void useDeleter() {
    //指定刪除動作  使用外面定義的函數(shù)
    ObjectPtr obj(new Object(2), deleterOfObject);
    ObjectPtr obj2 = obj;
    // obj 和 obj2 會在離開這個函數(shù)的時候析構(gòu)过咬,于是大渤,就調(diào)用了 deleterOfObject

    //管理數(shù)組  使用匿名函數(shù)當(dāng)作刪除函數(shù)
    std::shared_ptr<int> p(new int[10], [](int* p){
        std::cout << "delete[] p" << std::endl;
        delete[] p; //需要使用delete[]
    });

    // vector<> 沒必要使用智能指針, 不用new 和 delete... 內(nèi)部已經(jīng)管理了
}

int main() {
    useDeleter();
}

/*
init obj
delete[] p  //注意析構(gòu)和構(gòu)造的順序是相反的
delete obj 2
bye bye2
*/

shared_ptr主要就是利用變量出了作用域之后析構(gòu)函數(shù)一定能被調(diào)用到掸绞,哪怕是出現(xiàn)了異常泵三。

不要用一個原始的指針初始化多個shared_ptr

例如下面的例子:

#include <iostream>
#include <memory>

class Object {
public:
    Object(int id) : m_id(id) {
        std::cout << "init obj " << std::endl;
    }
    ~Object() {
        std::cout << "bye bye " << m_id << std::endl;
    }
    int id() const {
        return m_id;
    }
private:
      int m_id;
};

// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;

int main() {
    Object *obj = new Object(2);
    ObjectPtr p1(obj);
    ObjectPtr p2(obj);
    std::cout << p1.use_count() << "  " << p2.use_count() << std::endl;
    std::cout << "finished" << std::endl;
}

/*
init obj
1  1
finished
bye bye 2
bye bye 203200  //m_id成為了隨機數(shù)
*/

可以發(fā)現(xiàn),雖然是用的同一個指針初始化了兩個shared_ptr衔掸,但是這兩個shared_ptr沒有關(guān)聯(lián)切黔,它們的引用計數(shù)都是1,然后問題就發(fā)生了具篇,p2先析構(gòu)纬霞,于是引用計數(shù)變?yōu)榱?code>0,就開始刪除它管理的資源obj驱显,于是obj就被析構(gòu)了诗芜,這是還算正常,接著析構(gòu)p1埃疫,引用計算也變成了0伏恐,它也開始刪除自己管理的資源obj,相當(dāng)于多次delete了同一個對象栓霜,m_id成為了隨機數(shù)翠桦,這還算好的情況,如果Object內(nèi)部還有指針胳蛮,或者obj的地址被其他變量占據(jù)了销凑,delete掉這塊內(nèi)存就會發(fā)生嚴(yán)重的錯誤!而且不好發(fā)現(xiàn)原因仅炊。

將this指針正確的傳遞給shared_ptr

其實就是由于上面的原因斗幼,我們不可能傳遞this指針給shared_ptr,因為用同一個指針初始化兩個shared_ptr抚垄,它們之間并沒有關(guān)聯(lián)蜕窿,如下面的例子:

#include <iostream>
#include <memory>

class Y
{
public:
    std::shared_ptr<Y> f(){
        return std::shared_ptr<Y>(this);
    }
};

int main()
{
    std::shared_ptr<Y> p1(new Y());
    std::shared_ptr<Y> p2 = p1->f(); // p2是由this構(gòu)造共享智能指針
    std::cout << p1.use_count() << "  " << p2.use_count() << std::endl; // 1  1
}

從上面的例子可以看出,返回由this構(gòu)造的shared_ptr并沒有用呆馁,返回還可能造成嚴(yán)重錯誤(由于可能多次delete)桐经!解決辦法是繼承std::enable_shared_from_this<Y>,然后使用shared_from_this()構(gòu)造shared_ptr浙滤。

#include <iostream>
#include <memory>

class Y : public std::enable_shared_from_this<Y>
{
public:
    std::shared_ptr<Y> f(){
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Y> p1(new Y());
    std::shared_ptr<Y> p2 = p1->f(); // p2是由p1的thiss構(gòu)造共享智能指針
    std::cout << p1.use_count() << " " << p2.use_count() << std::endl; // 2  2

    std::shared_ptr<Y> p3(new Y());
    std::shared_ptr<Y> p4 = p3->f(); // p4是由p3的this構(gòu)造的構(gòu)造共享智能指針
    std::cout << p1.use_count() << " " << p2.use_count() << " "
              << p3.use_count() << " " << p4.use_count() << std::endl; // 2 2 2 2
}

可以發(fā)現(xiàn)引用計數(shù)確實增加了阴挣。并且由p1得到的shared_from_this()增加的就是p1的引用計數(shù),p3得到的shared_from_this()增加的就是p3的引用計數(shù)瓷叫,這和this的含義是一樣的屯吊。所以我們在類內(nèi)部需要傳遞this指針給shared_ptr時送巡,需要繼承自std::enable_shared_from_this<T>,并且使用shared_from_this()替代this盒卸。而shared_from_this()就是借助了weak_ptr骗爆。原理在后面再講。

shared_ptr的正確構(gòu)造方式

其實上面使用的智能指針構(gòu)造方式有一點點問題蔽介,ObjectPtr obj(new Object(1));這一個語句其實調(diào)用了兩次new摘投,一次是new Object(1),另一次是構(gòu)造內(nèi)部的引用計數(shù)變量的時候虹蓄,那有沒有辦法只掉用一次new呢犀呼,答案就是使用make_shared<T>()模板函數(shù),它將資源和引用計數(shù)變量一起new出來薇组,例如:

#include <iostream>
#include <memory>
#include <cassert>
class Object {
public:
    Object(int id) : m_id(id) {
        std::cout << "init obj " << std::endl;
    }
    ~Object() {
        std::cout << "bye bye " << m_id << std::endl;
    }
    int id() const {
        return m_id;
    }
private:
      int m_id;
};

// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;

int main() {
    // 和 ObjectPtr obj(new Object(2)); 一樣
    // 但是只調(diào)用了一次new
    ObjectPtr obj = std::make_shared<Object>(2);
}

然而外臂,這個函數(shù)也有失效的時候,如果管理的資源對象的構(gòu)造函數(shù)是私有的他就沒有辦法了律胀。

weak_ptr弱引用的智能指針

循環(huán)引用問題的引出

在有些情況下宋光,shared_ptr也會遇見很尷尬、不能處理的情況炭菌,那就是循環(huán)引用罪佳,看下面的例子:

#include <iostream>
#include <memory>

class Parent;  //Parent類的前置聲明

typedef std::shared_ptr<Parent> ParentPtr;

class Child {
public:
    ParentPtr father;
    ~Child() {
        std::cout << "bye child" << std::endl;
    }
};

typedef std::shared_ptr<Child> ChildPtr;

class Parent {
public:
    ChildPtr son;
    ~Parent() {
        std::cout << "bye parent" << std::endl;
    }
};

void testParentAndChild() {
    ParentPtr p(new Parent());  // 1  資源A
    ChildPtr c(new Child());  // 2   資源B
    p->son = c;     // 3      c.use_count() == 2 and p.use_count() == 1 
    c->father = p;    //  4   c.use_count() == 2 and p.use_count() == 2
}

int main() {
    testParentAndChild();
    std::cout << "finished" << std::endl;
}

/*
// 沒有調(diào)用Parent 和 Child 的析構(gòu)函數(shù)
finished
*/

很驚訝的發(fā)現(xiàn),用了shared_ptr管理資源黑低,資源最后還是沒有釋放赘艳!內(nèi)存泄漏還是發(fā)生了。

分析:

  1. 執(zhí)行編號1的語句時克握,構(gòu)造了一個共享智能指針p蕾管,稱呼它管理的資源叫做資源Anew Parent()產(chǎn)生的對象)吧, 語句2構(gòu)造了一個共享智能指針c玛荞,管理資源B(new Child()產(chǎn)生的對象)娇掏,此時資源AB的引用計數(shù)都是1呕寝,因為只有1個智能指針管理它們勋眯,執(zhí)行到了語句3的時候,是一個智能指針的賦值操作下梢,資源B的引用計數(shù)變?yōu)榱?code>2客蹋,同理,執(zhí)行完語句4孽江,資源A的引用計數(shù)也變成了2讶坯。
  2. 出了函數(shù)作用域時,由于析構(gòu)和構(gòu)造的順序是相反的岗屏,會先析構(gòu)共享智能指針c辆琅,資源B的引用計數(shù)就變成了1漱办;接下來繼續(xù)析構(gòu)共享智能指針p,資源A的引用計數(shù)也變成了1婉烟。由于資源AB的引用計數(shù)都不為1娩井,說明還有共享智能指針在使用著它們,所以不會調(diào)用資源的析構(gòu)函數(shù)似袁!
  3. 這種情況就是個死循環(huán)洞辣,如果資源A的引用計數(shù)想變成0,則必須資源B先析構(gòu)掉(從而析構(gòu)掉內(nèi)部管理資源A的共享智能指針)昙衅,資源B的引用計數(shù)想變?yōu)?code>0扬霜,又得依賴資源A的析構(gòu),這樣就陷入了一個死循環(huán)而涉。

要想解決這個問題著瓶,只能引入新的智能指針weak_ptr,顧名思義啼县,弱引用蟹但,也就是不增加引用計數(shù),它不管理shared_ptr內(nèi)部管理的指針谭羔,他只是起一個監(jiān)視的作用华糖。它監(jiān)視的不是shared_ptr本身,而是shared_ptr管理的資源N谅恪?筒妗!weak_ptr沒有重載操作符*->话告,它不能直接操作資源兼搏,但是它可以獲取所監(jiān)視的shared_ptr(如果資源還沒有被析構(gòu)的話)。

weak_ptr的基本用法

weak_ptr使用示例:

#include <iostream>
#include <memory>

class Object {
public:
    Object(int id) : m_id(id) {
        std::cout << "init obj " << std::endl;
    }
    ~Object() {
        std::cout << "bye bye" << m_id << std::endl;
    }
    int id() const {
        return m_id;
    }
private:
      int m_id;
};

// 取個別名 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;

void sharedPtrWithWeakPtr() {
    ObjectPtr obj(new Object(1));
    typedef std::weak_ptr<Object> WeakObjectPtr;
    WeakObjectPtr weakObj(obj); //使用共享指針 初始化 弱引用指針
    //weakObj 僅僅是一個監(jiān)聽者沙郭,不會增加引用計數(shù)
    std::cout << "obj use count is " << obj.use_count() << std::endl; // 1
    {
        // lock() 方法返回一個 它對應(yīng)的共享指針
        // 下面這句話的結(jié)果是 2, 而不是1佛呻,
        // 說明weakObj.lock() 內(nèi)部也得到了一個新的共享指針,所以引用計數(shù)+1
        // 在執(zhí)行完這句話后就析構(gòu)掉了病线,引用計數(shù)-1
        std::cout << "weakObj.lock().use_count() is " << weakObj.lock().use_count() << std::endl; // 2
        // 由于發(fā)生了一次 賦值 吓著,所以 引用次次數(shù) +1
        // auto === ObjectPtr
        auto p = weakObj.lock(); //如果weakObj監(jiān)視的資源存在, p就存在
        std::cout << "obj use count is " << obj.use_count() << std::endl; // 2
        if ( p ) {
            // do what you want to do
        } else {

        }
    }

    // 共享指針不再管理任何資源的時候送挑,weakObj的行為
    // 注意:如果在obj.reset前绑莺,還存在共享指針管理它的資源
    // 如 :ObjectPtr obj1(obj); weakObj.lock();還是有效的
    obj.reset();
    {
        auto p = weakObj.lock();
        if( p ) {
            //不應(yīng)該到這里來
            std::cout << "weakObj is not null 1" << std::endl;
        } else {
            std::cout << "weakObj is null 1" << std::endl;
        }
    }

    // 共享指針管理其他資源的時候,weakObj的行為
    // 注意:weak_ptr.lock()
    // 只有在 存在某一個shared_ptr管理的資源和該weak_ptr一樣 的時候才有效果惕耕!
    obj.reset(new Object(2));
    {
        auto p = weakObj.lock();
        if( p ) {
            //不應(yīng)該到這里來
            std::cout << "weakObj is not null 2" << std::endl;
        } else {
            std::cout << "weakObj is null 2" << std::endl;
        }
    }

    weakObj = obj; // 重新監(jiān)視  obj
    // 用weakObj 判斷管理的資源是否過期
    if(weakObj.expired()) {

    } else {

    }
}

int main() {
    sharedPtrWithWeakPtr();
    std::cout << "finished" << std::endl;
}
/*
init obj
obj use count is 1
weakObj.lock().use_count() is 2
obj use count is 2
bye bye1
weakObj is null 1
init obj
weakObj is null 2
bye bye2
finished
*/

由上面的例子可以看出纺裁,weak_ptr和初始化它的share_ptr沒有關(guān)系,而是和share_ptr管理的資源有關(guān)系。假如WeakObjectPtr weakObj(obj);欺缘,如果obj.reset()栋豫,weakObj.lock()的返回值就是空,如果obj.reset(new Object(2));谚殊,替換了管理對象笼才,則一起的資源就被析構(gòu)了,weakObj.lock()的返回值同樣為空络凿,同樣可以推斷骡送,如果除了obj以外還有其他共享智能指針一起管理資源,也就是說obj.reset()的時候資源不會被析構(gòu)絮记,weakObj.lock();的返回值就不會為空了摔踱。不明白的話自己寫個簡單的測試用例就知道了,如:

void sharedPtrWithWeakPtr() {
    ObjectPtr obj(new Object(1));
    typedef std::weak_ptr<Object> WeakObjectPtr;
    WeakObjectPtr weakObj(obj); //使用共享指針 初始化 弱引用指針
    ObjectPtr obj1 = obj; //注釋掉這句話打印的就是error怨愤, 加上這句話打印的就是ok
    obj.reset();
    auto p = weakObj.lock();
    if( p ) {
        std::cout << "ok" << std::endl;
    } else {
        std::cout << "error" << std::endl;
    }
}

weak_ptr解決循環(huán)引用

weak_ptr可以解決上面的循環(huán)引用問題派敷,將Child內(nèi)部的parent指針換成weak_ptr管理:

#include <iostream>
#include <memory>

class Parent;  //Parent類的前置聲明

typedef std::shared_ptr<Parent> ParentPtr;
typedef std::weak_ptr<Parent> WeakParentPtr;
class Child {
public:
    WeakParentPtr father;
    ~Child() {
        std::cout << "bye child" << std::endl;
    }
};

typedef std::shared_ptr<Child> ChildPtr;
//typedef std::weak_ptr<Child> WeakChildPtr;
class Parent {
public:
    //WeakChildPtr son;
    ChildPtr son;
    ~Parent() {
        std::cout << "bye parent" << std::endl;
    }
};

void testParentAndChild() {
    ParentPtr p(new Parent());  // 1  資源A
    ChildPtr c(new Child());  // 2   資源B
    p->son = c;     // 3      c.use_count() == 2 and p.use_count() == 1
    c->father = p;    //  4   c.use_count() == 2 and p.use_count() == 1
}

int main() {
    testParentAndChild();
    std::cout << "finished" << std::endl;
}
/*
bye parent //成功調(diào)用析構(gòu)函數(shù)
bye child
finished
*/

修改為弱引用后,成功的釋放了資源撰洗,只要將任意一個shared_ptr換成weak_ptr篮愉,就可以解決問題。當(dāng)然差导,也可以兩個都換成weak_ptr试躏,至于這三種方案誰更好,就暫時不清楚了设褐。

shared_from_this()實現(xiàn)原理

std::enable_shared_from_this<T>模板類中有一個weak_ptr颠蕴,這個weak_ptr用來觀測this智能指針,調(diào)用shared_from_this()函數(shù)的時候助析,會在內(nèi)部調(diào)用weak_ptrlock()方法犀被,將所觀測的shared_ptr返回。這個設(shè)計要依賴于當(dāng)前對象已經(jīng)有了一個相應(yīng)的控制塊外冀。為此寡键,必須已經(jīng)存在一個指向當(dāng)前對象的shared_ptr(比如在調(diào)用過shared_from_this()成員函數(shù)之外已經(jīng)有了一個)。假如沒有這樣shared_ptr存在雪隧,那么shared_from_this()會拋異常西轩。 那么這個weak_ptr在什么時候賦值的呢?答案就是在外部第一次構(gòu)造shared_ptr的時候(如之前的std::shared_ptr<Y> p1(new Y());)膀跌,對std::enable_shared_from_this<T>進行了賦值(具體實現(xiàn)有點復(fù)雜遭商,還不太懂。捅伤。),這也就為什么在調(diào)用shared_from_this()時巫玻,必須存在一個指向當(dāng)前對象的shared_ptr的原因了丛忆。由于這個原因祠汇,不要在構(gòu)造函數(shù)中調(diào)用shared_from_this(),如:

#include <iostream>
#include <memory>

class Y : public std::enable_shared_from_this<Y>
{
public:
    Y() {
        std::shared_ptr<Y> p = shared_from_this();
    }

    std::shared_ptr<Y> f(){
        return shared_from_this();
    }
};

int main()
{
    std::shared_ptr<Y> p1(new Y());

}
// 會拋出異常熄诡!
/*
terminate called after throwing an instance of 'std::bad_weak_ptr'
  what():  bad_weak_ptr
*/

unique_ptr獨占的智能指針

unique_ptr相對于其他兩個智能指針更加簡單可很,它和shared_ptr使用差不多,但是功能更為單一凰浮,它是一個獨占型的智能指針我抠,不允許其他的智能指針共享其內(nèi)部的指針,更像原生的指針(但更為安全袜茧,能夠自己釋放內(nèi)存)菜拓。不允許賦值和拷貝操作,只能夠移動笛厦。

#include <iostream>
#include <memory>
#include <cassert>
class Object {
public:
    Object(int id) : m_id(id) {
        std::cout << "init obj " << std::endl;
    }
    ~Object() {
        std::cout << "bye bye " << m_id << std::endl;
    }
    int id() const {
        return m_id;
    }
private:
      int m_id;
};

// 讓寫起來更方便
typedef std::shared_ptr<Object> ObjectPtr;
typedef std::unique_ptr<Object> UniqueObjectPtr;

// 只能傳遞引用 不能傳值
void print(const UniqueObjectPtr& obj) {
    std::cout << obj->id() << std::endl;
}

int main() {
    UniqueObjectPtr obj(new Object(1));
//    UniqueObjectPtr obj1 = obj; // 編譯錯誤纳鼎,不允許賦值
    // 獲取原生指針
    auto p = obj.get();
    if( p ) {

    } else {

    }

    // better 重載了 operator bool
    if(obj) {

    } else {

    }
    // 重載了 operator -> 和 operator *
    std::cout << obj->id() << "  " << (*obj).id() << std::endl;
    print(obj);
    // 釋放管理的指針,由其他東西處理
    p = obj.release();
//    delete p; 自己負(fù)責(zé)處理
    obj.reset(p); //析構(gòu)之前負(fù)責(zé)管理的對象裳凸,重新管理 p指針
    obj.reset(); // 析構(gòu)之前負(fù)責(zé)管理的對象贱鄙, 不再管理任何資源
    // 允許 移動操作
    UniqueObjectPtr obj1(new Object(1));
    // obj1已經(jīng)部管理任何資源 obj2開始管理obj1之前的資源
    UniqueObjectPtr obj2 = std::move(obj1);
    assert(obj1 == nullptr);
    std::cout << obj2->id() << std::endl;

    // 將unique_ptr管理的內(nèi)容給 shared_ptr
    ObjectPtr obj3(std::move(obj2));
    assert(obj2 == nullptr);

}

/*
init obj
1  1
1
bye bye 1
init obj
1
bye bye 1
*/

unique_ptr管理數(shù)組資源不需要指定刪除器:

std::shared_ptr<int> p(new int[10], [](int* p){
            std::cout << "delete[] p" << std::endl;
            delete[] p; //需要使用delete[]
        });
std::unique_ptr<int> p2(new int[10]); //不需要指定刪除器

性能與安全的權(quán)衡

使用智能指針雖然能夠解決內(nèi)存泄漏問題,但是也付出了一定的代價姨谷。以shared_ptr舉例:

  1. shared_ptr的大小是原始指針的兩倍逗宁,因為它的內(nèi)部有一個原始指針指向資源,同時有個指針指向引用計數(shù)梦湘。
  2. 引用計數(shù)的內(nèi)存必須動態(tài)分配疙剑。雖然一點可以使用make_shared()來避免,但也存在一些情況下不能夠使用make_shared()践叠。
  3. 增加和減小引用計數(shù)必須是原子操作言缤,因為可能會有讀寫操作在不同的線程中同時發(fā)生。比如在一個線程里有一個指向一塊資源的shared_ptr可能調(diào)用了析構(gòu)(因此所指向的資源的引用計數(shù)減一)禁灼,同時管挟,在另一線程里,指向相同對象的一個shared_ptr可能執(zhí)行了拷貝操作(因此弄捕,引用計數(shù)加一)僻孝。原子操作一般會比非原子操作慢。但是為了線程安全穿铆,又不得不這么做斋荞,這就給單線程使用環(huán)境帶來了不必要的困擾。

我覺得還是分場合吧,看應(yīng)用場景來進行權(quán)衡凤优,我也沒啥經(jīng)驗悦陋,但我感覺安全更重要筑辨,現(xiàn)在硬件已經(jīng)足夠快了,其他例如java這種支持垃圾回收的語言不還是用的很好嗎棍辕。

總結(jié)

  • 智能指針主要是使用構(gòu)造和析構(gòu)來管理資源的暮现。
  • shared_ptr很好用也很難用,有兩種構(gòu)造方式栖袋,使用引用計數(shù)實現(xiàn)多人同時管理一份資源哪替。使用this的時候要格外注意凭舶。
  • weak_ptr可以解決shared_ptr的循環(huán)引用問題。
  • unique_ptr最像裸指針帅霜,但更為安全身冀,保證資源的釋放,不能復(fù)制只能移動珍促。
  • 智能指針帶來了性能問題剩愧,在不同場合可以選擇不同的解決方案。優(yōu)先使用類的實例(如果內(nèi)存足夠)穴翩,其次unique_ptr芒帕,最后才是shared_ptr丰介。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鉴分,一起剝皮案震驚了整個濱河市冠场,隨后出現(xiàn)的幾起案子本砰,更是在濱河造成了極大的恐慌钢悲,老刑警劉巖莺琳,帶你破解...
    沈念sama閱讀 207,248評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異珍手,居然都是意外死亡琳要,警方通過查閱死者的電腦和手機秤茅,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評論 2 381
  • 文/潘曉璐 我一進店門框喳,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人乍惊,你說我怎么就攤上這事放仗〕准啵” “怎么了?”我有些...
    開封第一講書人閱讀 153,443評論 0 344
  • 文/不壞的土叔 我叫張陵稼钩,是天一觀的道長达罗。 經(jīng)常有香客問我,道長巡李,這世上最難降的妖魔是什么侨拦? 我笑而不...
    開封第一講書人閱讀 55,475評論 1 279
  • 正文 為了忘掉前任狱从,我火速辦了婚禮,結(jié)果婚禮上敞葛,老公的妹妹穿的比我還像新娘与涡。我一直安慰自己,他們只是感情好氨肌,可當(dāng)我...
    茶點故事閱讀 64,458評論 5 374
  • 文/花漫 我一把揭開白布儒飒。 她就那樣靜靜地躺著檩奠,像睡著了一般埠戳。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颗圣,一...
    開封第一講書人閱讀 49,185評論 1 284
  • 那天屁使,我揣著相機與錄音蛮寂,去河邊找鬼。 笑死及老,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的食铐。 我是一名探鬼主播僧鲁,決...
    沈念sama閱讀 38,451評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼悔捶,長吁一口氣:“原來是場噩夢啊……” “哼单芜!你這毒婦竟也來了洲鸠?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,112評論 0 261
  • 序言:老撾萬榮一對情侶失蹤绢淀,失蹤者是張志新(化名)和其女友劉穎皆的,沒想到半個月后蹋盆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡楞抡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,083評論 2 325
  • 正文 我和宋清朗相戀三年召廷,在試婚紗的時候發(fā)現(xiàn)自己被綠了竞慢。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片治泥。...
    茶點故事閱讀 38,163評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡坤按,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情揽咕,我是刑警寧澤粟关,帶...
    沈念sama閱讀 33,803評論 4 323
  • 正文 年R本政府宣布闷板,位于F島的核電站,受9級特大地震影響性昭,放射性物質(zhì)發(fā)生泄漏县遣。R本人自食惡果不足惜萧求,卻給世界環(huán)境...
    茶點故事閱讀 39,357評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望元旬。 院中可真熱鬧守问,春花似錦酪碘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽钦幔。三九已至,卻和暖如春搀擂,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背喷市。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評論 1 261
  • 我被黑心中介騙來泰國打工品姓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留箫措,地道東北人斤蔓。 一個月前我還...
    沈念sama閱讀 45,636評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像惧互,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拨与,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,925評論 2 344

推薦閱讀更多精彩內(nèi)容

  • 原作者:Babu_Abdulsalam 本文翻譯自CodeProject买喧,轉(zhuǎn)載請注明出處淤毛。 引入### Ooops...
    卡巴拉的樹閱讀 30,072評論 13 74
  • 1.C和C++的區(qū)別?C++的特性姓言?面向?qū)ο缶幊痰暮锰帲?答:c++在c的基礎(chǔ)上增添類何荚,C是一個結(jié)構(gòu)化語言猪杭,它的重...
    杰倫哎呦哎呦閱讀 9,478評論 0 45
  • 本文版權(quán)歸 liancheng 所有皂吮,如有轉(zhuǎn)載請按如下方式標(biāo)明原創(chuàng)作者及出處税手,以示尊重P枘伞候齿!原創(chuàng)作者:lianche...
    周肅閱讀 7,587評論 2 6
  • 前言 把《C++ Primer》[https://book.douban.com/subject/25708312...
    尤汐Yogy閱讀 9,506評論 1 51
  • C++智能指針 原文鏈接:http://blog.csdn.net/xiaohu2022/article/deta...
    小白將閱讀 6,853評論 2 21