首頁 > 軟體

C++Smart Pointer 智慧指標詳解

2022-03-14 13:00:56

一、為啥使用智慧指標呢

標準庫中的智慧指標:
std::auto_ptr    --single ownership  (C++98中出現,缺陷較多,被摒棄)
std::unique_ptr  --single ownership  (C++11替代std::auto_ptr,用於單執行緒)
std::shared_ptr  --shared ownership  (C++11,用於多執行緒)
std::weak_ptr    --temp/no ownership (C++11)
Introduced in C++ 11
Defined in <memory> header.

首先看一個下面的栗子,左邊是木有使用智慧指標的情況,當執行foo()函數,其中的e指標會在bar(e)時傳入bar函數,但是在bar函數結束後沒有人為delete e時,就會導致記憶體漏失;但是在右邊的栗子中,使用了unique_ptr智慧指標(single ownership),就能防止記憶體漏失。

智慧指標主要用於管理在堆上分配的記憶體,它將普通的指標封裝為一個棧物件。當棧物件的生存週期結束後,會在解構函式中釋放掉申請的記憶體,從而防止記憶體漏失。

  • auto_ptr智慧指標:(C++11出來前只有這種智慧指標)當物件拷貝或者賦值後,前面的物件就懸空了。
  • unique_ptr智慧指標:防止智慧指標拷貝和複製。
  • shared_ptr智慧指標:通過參照計數的方式來實現多個shared_ptr物件之間共用資源。
  • weak_ptr智慧指標:可以從一個shared_ptr或另一個weak_ptr物件構造,它的構造和解構不會引起參照記數的增加或減少。

注意:每一種智慧指標都可以增加記憶體的參照計數。

  • 智慧指標分為兩類:
    • 一種是可以使用多個智慧指標管理同一塊記憶體區域,每增加一個智慧指標,就會增加1次參照計數,
    • 另一類是不能使用多個智慧指標管理同一塊記憶體區域,通俗來說,當智慧指標2來管理這一塊記憶體時,原先管理這一塊記憶體的智慧指標1只能釋放對這一塊指標的所有權(ownership)。
  • 按照這個分類標準,auto_ptr unique_ptr weak_ptr屬於後者,shared_ptr屬於前者。

shared_ptr進行初始化時不能將一個普通指標直接賦值給智慧指標,因為一個是指標,一個是類。可以通過make_shared函數或者通過建構函式傳入普通指標。並可以通過get函數獲得普通指標。

#include <string>
#include <memory>
using namespace std;
class report
{
private:
    string str;
public:
    report(const string s):str(s) //構造方法
    {
        cout<<"1 report Object  has been build!"<<endl;
    }
    ~report()
    {
        cout<<"3 report Object  deleted!"<<endl;
    }
    void talk()
    {
        cout<<str<<endl;
    }
};
int main()
{
    string talk="2 hello,this is a test!";
    {
        auto_ptr<report> ptr(new report(talk));
        ptr->talk();
    }
    {
        shared_ptr<report> ptr(new report(talk));
        ptr->talk();
    }
    {
        unique_ptr<report> ptr(new report(talk));
        ptr->talk();
    }
    return 0;
}

二、shared_ptr智慧指標

shared_ptr實現了共用擁有的概念,利用“參照計數”來控制堆上物件的生命週期。

share_ptr的生命週期:

原理:在初始化的時候參照計數設為1,每當被拷貝或者賦值的時候參照計數+1,解構的時候參照計數-1,直到參照計數被減到0,那麼就可以delete掉物件的指標了。他的構造方式主要有以下三種:

shared_ptr<Object> ptr;
shared_ptr<Object> ptr(new Object);
shared_ptr<Object> ptr(new Object, [=](Object *){ //回收資源時呼叫的函數 });
auto ptr = make_shared<Object>(args);
  • 第一種空構造,沒有指定shared_ptr管理的堆上物件的指標,所以參照計數為0,後期可以通過reset()成員函數來指定其管理的堆上物件的指標,reset()之後參照計數設為1。
  • 第二種是比較常見的構造方式,建構函式裡面可以放堆上物件的指標,也可以放其他的智慧指標(如weak_ptr)。
  • 第三種構造方式指定了shared_ptr在解構自己所儲存的堆上物件的指標時(即參照計數為0時)所要呼叫的函數,這說明我們可以自定義特定物件的特定解構方式。同樣的,reset()成員函數也可以指定解構時呼叫的指定函數。
  • 第四種方法:較常見,構造shared_ptr的方式(最安全):
auto ptr = make_shared<Object>(args);

上面第四種方法,使用標準庫裡邊的make_shared<>()模板函數。該函數會呼叫模板類的構造方法,範例化一個堆上物件,然後將儲存了該物件指標的shared_ptr返回。引數是該類建構函式的引數,所以使用make_shared<>()就好像單純地在構造該類物件一樣。auto是C++11的一個關鍵字,可以在編譯期間自動推算變數的型別,在這裡就是shared_ptr<Object>型別。

shared_ptr的其他成員函數:

use_count()	//返回參照計數的個數
unique()	//返回是否是獨佔所有權(use_count是否為1)
swap()		//交換兩個shared_ptr物件(即交換所擁有的物件,參照計數也隨之交換)
reset()		//放棄內部物件的所有權或擁有物件的變更, 會引起原有物件的參照計數的減少

三、unique_ptr智慧指標

注意unique_ptr是single ownership的,不能拷貝。其構造方式如下:

unique_ptr的生命週期:

四、weak_ptr智慧指標

五、智慧指標怎麼解決交叉參照,造成的記憶體漏失

結論:建立物件時使用shared_ptr強智慧指標指向,其餘情況都使用weak_ptr弱智慧指標指向。

5.1 交叉參照的栗子:

當A類中有一個指向B類的shared_ptr強型別智慧指標,B類中也有一個指向A類的shared_ptr強型別智慧指標。

main函數執行後有兩個強智慧指標指向了物件A,物件A的參照計數為2,B類也是:

#include <iostream>
#include <memory>
using namespace std;
class B;
class A{
public:
    shared_ptr<B> _bptr;
};
class B{
public:
    shared_ptr<A> _aptr;
};
int main(){
    shared_ptr<A> aptr(new A());
    shared_ptr<B> bptr(new B());
    aptr->_bptr = bptr;
    bptr->_aptr = aptr;
    return 0;
}

而當主函數mainreturn返回後,物件A的參照計數減一變為1(aptr沒指向A物件了),B物件也是,參照計數不為0,即不能解構2個物件釋放記憶體,造成記憶體漏失。

5.2 解決方案

將類A和類B中的shared_ptr強智慧指標都換成weak_ptr弱智慧指標;

class A{
public:
    weak_ptr<B> _bptr;
};
class B{
public:
    weak_ptr<A> _aptr;
};

weak_ptr弱智慧指標,雖然有參照計數,但實際上它並不增加計數,而是隻觀察物件的參照計數。所以此時物件A的參照計數只為1,物件B的參照計數也只為1。

六、智慧指標的注意事項

  • 避免同一塊記憶體系結到多個獨立建立的shared_ptr上,因此要不使用相同的內建指標初始化(或reset)多個智慧指標,不要混合使用智慧指標和普通指標,堅持只用智慧指標。
  • 不delete get() 函數返回的指標,因為這樣操作後,shared_ptr並不知道它管理的記憶體被釋放了,會造成shared_ptr重複解構。
  • 不使用 get()函數初始化或(reset)另外的智慧指標。
shared_ptr<int> p = make_share<int> (42);
int *q = p.get();
{
  shared_ptr<int>(q); 
} // 程式塊結束,q被銷燬,指向的記憶體被釋放。
int foo = *p; //  出錯,p指向的記憶體已經被q釋放,這是用get() 初始化另外的智慧指標惹得禍。
// 請記住,永遠不要用get初始化另外一個智慧指標。

能使用unique_ptr時就不要使用share_ptr指標(後者需要保證執行緒安全,所以在賦值or銷燬時overhead開銷更高)。

總結

本篇文章就到這裡了,希望能夠給你帶來幫助,也希望您能夠多多關注it145.com的更多內容!         


IT145.com E-mail:sddin#qq.com