C++ 智能指針 shared_ptr 分析


引文:

C++對指針的管理提供了兩種解決問題的思路:

1.不允許多個對象管理一個指針

2.允許多個對象管理一個指針,但僅當管理這個指針的最后一個對象析構時才調用delete

ps:這兩種思路的共同點就是只允許delete一次,下面將討論的shared_ptr就是采用思路1實現的

ps:智能指針不是指針,而是類,可以實例化為一個對象,來管理裸指針


1.shared_ptr的實現原理:

shared_ptr最本質的功能:“當多個shared_ptr管理同一個指針,僅當最后一個shared_ptr析構時,指針才被delete”,該功能是通過引用計數法實現的

引用計數法的規則:

  1)所有管理同一個裸指針的shared_ptr,都共享一個引用計數器

  2)每當一個shared_ptr被賦值給其他shared_ptr時,這個共享的引用計數器就加1

  3)每當一個shared_ptr析構或被用於管理其他裸指針時,這個引用計數器就減1

  4)如果此時發現引用計數器為0,那么說明它是管理這個指針的最后一個shared_ptr了,於是我們釋放指針指向的資源

引用計數法的內部實現:

  1)這個引用計數器保存在某個內部類型中,而這個內部類型對象在shared_ptr第一次構造時以指針的形式保存在shared_ptr中

  2)shared_ptr重載了賦值運算符,在賦值和拷貝另一個shared_ptr時,這個指針被另一個shared_ptr共享

  3)在引用計數歸0時,這個內部類型指針與shared_ptr管理的資源一起釋放

  4)此外,為了保證線程安全,引用計數器的加1和減1都是原子操作,它保證了shared_ptr由多個線程共享時不會爆掉


2.shared_ptr的使用

#include<iostream>
#include<stdio.h>
#include<string>
#include<memory>
using namespace std;

int main()
{
    //初始化 方法1:
    shared_ptr<string> sptr1(new string("name"));
    //初始化 方法2:
    shared_ptr<string> sptr2=make_shared<string>("sex");
    //初始化 方法3:
    int *p =new int(10);
    shared_ptr<int> sptr3(p); //這種初始化的方式很危險,delete p之后,strp3也不再有效
}

相關成員函數:

1)use_count:返回引用計數的個數

2)unique:返回是否獨占所有權(use_count=1)

3)swap:交換兩個share_ptr對象(即交換所擁有的對象)

4)reset:放棄內部對象的所有權或擁有對象的變更,會引起原有對象引用計數的減少

5)get:返回內部對象指針


3.引用計數最大的缺點:循環引用

下面是事故現場:

class Observer; // 前向聲明
class Subject
{
private:

    std::vector<shared_ptr<Observer>> observers;
public:
    Subject() {}
    addObserver(shared_ptr<Observer> ob)
    {
        observers.push_back(ob);
    }
    // 其它代碼
};

class Observer
{
private:
    shared_ptr<Subject> object;
public:
    Observer(shared_ptr<Object> obj) : object(obj) {}
    
    // 其它代碼
};

目標類subject連接這多個觀察者類,當某個事件發生時,目標類可以遍歷觀察者數組observers,對觀察者進行通知,而觀察者類中也保留着目標類的shared_ptr,這樣多個觀察者之間可以以目標類為橋梁進行溝通,除了會發生內存泄漏外,這還是一種很不錯的設計模式嘛……

這里產生內存泄漏的原因就是循環引用,循環引用指的是一個引用通過一系列的引用鏈,竟然引回到自身,在上面的例子中,subject->observer->subject就是這么一條環形引用鏈,假設我們程序中只有一個變量shared_ptr<sbuject> p,此時p指向的對象不僅通過shared_ptr引向自己,還通過它包含的observer中的object成員變量引回自己,於是它的引用計數是2,每個observer的引用計數都是1,當p析構時,它的引用計數2-1=1,大於0,其析構函數不會被調用,於是p和它包含的每個observer對象在程序結束時依然駐留在內存中,沒有被delete,從而造成了內存泄漏


4.采用weak_ptr(弱引用)解決循環引用的問題:

標准庫提供了std::weak_ptr,weak_ptr是shared_ptr的觀察者,它與一個shared_ptr綁定,但是卻不參與引用計數的計算,在需要時,它還能生成一個與它所觀察的shared_ptr共享引用計數器的新的shared_ptr,總而言之,weak_ptr的作用就是:在需要時生成一個與綁定的shared_ptr共享引用計數器的新shared_ptr,在其他時候不干擾綁定的shared_ptr的引用計數

weak_ptr相關成員函數:

1)lock:獲得一個和綁定的shared_ptr共享引用計數器的新的shared_ptr

2)expired:功能等價於判斷use_count是否等於0,但是速度更快

繼續引用上面subject和observer的例子,來解決循環引用的問題:

將上述例子中,observer中object成員的類型換成weak_ptr<subject>即可解決內存泄漏的問題,因為之前的observer中object成員的subject參與了引用計數,替換成weak_ptr<subject>之后沒有參與引用計數,這樣以來,p指向對象的引用計數為1,所以在p析構時,subject指針將被delete,其中包含的observer數組在析構時,內部的observer對象的引用計數也為0,所以他們也被deleete了,不存在內存泄漏的問題了

class Observer; // 前向聲明
class Subject
{
private:

    std::vector<shared_ptr<Observer>> observers;
public:
    Subject() {}
    addObserver(shared_ptr<Observer> ob)
    {
        observers.push_back(ob);
    }
    // 其它代碼
};

class Observer
{
private:
    shared_ptr< weak_ptr<Subject> > object;
public:
    Observer(shared_ptr<Object> obj) : object(obj) {}
    
    // 其它代碼
};


5.錯誤用法1:多個無關的shared_ptr管理同一個裸指針,有可能導致二次析構

int main()
{
    int *a = new int(10);

    shared_ptr<int> p1(a);

    shared_ptr<int> p2(a);
}

p1和p2管理同一個裸指針a,此時的p1和p2有着完全獨立的兩個引用計數器,所以p1析構的時候會將a析構一次,p2析構的時候也會將a析構一次,C++中不允許同一個東西被析構兩次,這樣會導致程序爆炸

為了避免這種情況,我們永遠不要將new用在shared_ptr構造函數列表以外的地方,或者干脆不用new,改用make_shared

另外,即使這樣,也有可能導致二次析構,比如我們采用shared_ptr的get函數獲得原始裸指針來構造另一個shared_ptr

class A
{
public:
    std::shared_ptr<A> getShared()
    {
        return std::shared_ptr<A>(this);
    }
};

int main()
{
    std::shared_ptr<A> pa = std::make_shared<A>();
    std::shared_ptr<A> pbad = pa->getShared();
}

上面的樣例中,pa和pbad各自擁有一個獨立的引用計數器,也有可能會導致二次析構

總而言之:管理同一個資源的sahred_ptr,只能由同一個初始shared_ptr通過一系列賦值和拷貝構造得到,要確保其共享的是同一個引用計數器


6.錯誤用法2:直接用new構造多個shared_ptr作為實參,可能會導致內存泄漏

// 聲明
void f(A *p1, B *p2);

// 使用
f(new A, new B);

上面的代碼很容易發生內存泄漏,假如new A先發生於new B,那么如果new B拋出異常,那么new A的分配將會發生泄漏

如果按照這種方式new多個share_ptr作為實參,依然會發生內存泄漏

//聲明
void f(shared_ptr<A> p1,shared_ptr<B> p2);

//使用
f(shared_ptr<A> (new A),shared_ptr<B>(new B));

因為shared_ptr的構造有可能發生在new A和new B之后,這里涉及到C++操作的sequence after性質,該性質保證:

1)new A發生在shared_ptr<A>構造發生之前

2)new B發生在shared_ptr<B>構造發生之前

3)兩個shared_ptr的構造發生在函數f的調用之前

在滿足上面三條性質的前提下,各操作的順序可以任意執行

若不使用new而是使用make_shared來構造shared_ptr,那么就不會產生內存泄漏

//聲明
void f(shared_ptr<A> p1,shared_ptr<B> p2);

//使用
f(make_shared<A>(),make_shared<B>());

原因很簡單,依然是sequence after性質,如果兩個函數的執行順序不確定,那么當一個函數執行時,另外一個函數不會執行,於是make_shared<A>的構造完成了,即使make_shared<B>的構造拋出了異常,那么A的資源也能夠被正確的釋放,和上面的情形相比較,make_shared保證了第二個new發生的時候,第一個new所分配的資源已經被shared_ptr管理起來了,所以在異常發生時,能夠正確的釋放資源

總結:請總是使用make_shared來生成shared_ptr


7.如果希望使用shared_ptr來管理動態數組,那么需要提供一個自定義的刪除器來代替delete

#include <iostream>
#include<memory>
using namespace std;

class DelTest
{
public:
    DelTest(){
        j= 0;
        cout<<" DelTest()"<<":"<<i++<<endl;
    }
    ~DelTest(){
        i = 0;
        cout<<"~ DelTest()"<<":"<<i++<<endl;
    }
    static int i,j;
};

int DelTest::i = 0;
int DelTest::j = 0;

void noDefine()
{
    cout<<"no_define start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10]);

}

void slefDefine()
{
    cout<<"slefDefine start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!傳入lambada表達式代替delete操作。
}

int main()
{
    noDefine();//!構造10次,析構1次。內存泄漏。
    cout<<"--------------------"<<endl;
    slefDefine();//!構造次數==析構次數 無內存泄漏
}
/*
運行結果:
no_define start running!
 DelTest():0
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
~ DelTest():0
--------------------
slefDefine start running!
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
 DelTest():10
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
*/

需要注意的是:雖然通過自定義刪除器的方式shared_ptr可以管理動態數組,但是shared_ptr並不支持下標運算符的操作,而且只能指針類型不支持指針算術運算(不能取地址),因此為了訪問數組中的元素,必須用get獲得一個原始內置裸指針,然后用它來訪問數組元素

樣例如下:

#include <iostream>
#include<memory>
using namespace std;

class DelTest
{
public:
    DelTest(){
        j= 0;
        x=i;
        cout<<" DelTest()"<<":"<<i++<<endl;
    }
    ~DelTest(){
        i = 0;
        cout<<"~ DelTest()"<<":"<<i++<<endl;
    }
    static int i,j;
    int x;
};

int DelTest::i = 0;
int DelTest::j = 0;

void noDefine()
{
    cout<<"no_define start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10]);

}

void slefDefine()
{
    cout<<"slefDefine start running!"<<endl;
    shared_ptr<DelTest> p(new DelTest[10],[](DelTest *p){delete[] p;});//!傳入lambada表達式代替delete操作。
    cout<<p.get()[4].x<<endl;

}

int main()
{
    noDefine();//!構造10次,析構1次。內存泄漏。
    cout<<"--------------------"<<endl;
    slefDefine();//!構造次數==析構次數 無內存泄漏
}
/*
運行結果:
no_define start running!
 DelTest():0
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
~ DelTest():0
--------------------
slefDefine start running!
 DelTest():1
 DelTest():2
 DelTest():3
 DelTest():4
 DelTest():5
 DelTest():6
 DelTest():7
 DelTest():8
 DelTest():9
 DelTest():10
5
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
~ DelTest():0
*/


8.使用shared_ptr管理非常規的動態對象的時候,記得自定義刪除器

某些情況下,有些動態內存也不是我們new出來的,如果要使用shared_ptr管理這種動態內存,也要自定義刪除器

#include <iostream>
#include <stdio.h>
#include <memory>
using namespace std;

void closePf(FILE * pf)//即可以避免異常發生后無法釋放內存的問題,也避免了很多人忘記執行fclose
{
    cout<<"----close pf after works!----"<<endl;
    fclose(pf);
}

int main()
{
    shared_ptr<FILE> pf(fopen("bin2.txt", "w"),closePf);
    cout<<"*****start working****"<<endl;
    if(!pf)
        return -1;
    char *buf = "abcdefg";
    fwrite(buf,8,1,pf.get());//確保fwrite不會刪除指針的情況下,可以將shared_ptr內置指針取出
    cout<<"------write in file!-----"<<endl;
}
/*
*****start working****
------write in file!-----
----close pf after works!----
*/

類比TCP/IP中連接打開和關閉的情況,同理都可以使用shared_ptr來管理


總結:

1)不用使用相同的內置/原始/裸指針初始化多個智能指針

2)不要delete get函數返回的指針

3)如果你使用了get返回的指針,記住當最后一個對應的智能指針銷毀后,你的指針就變為無效了

4)如果你使用的智能指針管理的資源不是new分配的內存,記得傳遞一個刪除器

5)請勿使用new構造多個shared_ptr作為實參,應該使用make_shared

6)存在循環引用關系時,請使用weak_ptr來保證不會產生內存泄漏



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM