天天看點

【C++】不能兩次殺死同一條魚 - 淺述shared_ptr智能指針的使用方法

作者:海洋餅幹叔叔

在C++裡,動态對象的建立是通過new操作符進行的,在恰當的時候通過delete操作符釋放動态對象的空間并執行其析構函數是程式員的職責。遺憾的是,多數新手程式員都做不好這項工作,相關的疏失導緻了巨量的軟體缺陷:

•未能釋放動态對象,導緻記憶體洩漏。

•在記憶體釋放後再次通路指針所指向的動态對象。在釋放指針所指向的動态對象後,及時将指針置為空對避免該問題的發生有幫助。

•多次釋放同一個動态對象。這種情況多發生在兩個以上的指針指向同一個動态對象時。

知識産權協定

允許以教育/教育訓練為目的向學生或閱聽人進行免費引用,展示或者講述,無須取得作者同意。

不允許以電子/紙質出版為目的進行摘抄或改編。

以少許效率損失為代價,智能指針可以部分解決此問題。本節以shared_ptr為例,簡要描述智能指針的使用方法及基本工作原理。請閱讀下述C++代碼。

//Project - SharedPointer
#include <iostream>
#include <memory>
using namespace std;

class Fish {
public:
    string sName;
    Fish(const string& name){
        sName = name;
        cout << "Fish Constructor called: " << sName << endl;
    }

    void sayHello(){
        cout << "Aloha: " << sName << endl;
    }

    ~Fish(){
        cout << "Fish Destructor called:  "  << sName << endl;
    }
};

void sayHello(shared_ptr<Fish> f){
    f->sayHello();      //對智能指針使用指向操作符->
}

void sayHello(Fish& f){
    f.sayHello();
}

int main(){
    shared_ptr<Fish> dora1(new Fish("Dora"));
    shared_ptr<Fish> tom1 = make_shared<Fish>("Tom");
    cout << "-----------------------------------------" << endl;

    sayHello(tom1);
    auto tom2 = tom1;   //智能指針對象的複制
    sayHello(*tom2);    //對智能指針使用解引用操作符*
    cout << "-----------------------------------------" << endl;

    dora1->sayHello();
    Fish* dora2 = dora1.get();   //擷取智能指針内的原始指針
    dora2->sayHello();
    cout << "-----------------------------------------" << endl;
    return 0;
}           

上述程式的執行結果為:

Fish Constructor called: Dora
Fish Constructor called: Tom
-----------------------------------------
Aloha: Tom
Aloha: Tom
-----------------------------------------
Aloha: Dora
Aloha: Dora
-----------------------------------------
Fish Destructor called:  Tom
Fish Destructor called:  Dora           

<memory>頭檔案引入了共享型智能指針模闆類shared_ptr。

要點 shared_ptr<T>表示一個指向T類型對象的智能指針對象。智能指針對象不是一個平凡的指針,而是一個包含平凡指針的對象,它通過引用計數來記錄指針所指向的對象的被引用次數,當被指向對象的引用計數降到0時(意味着動态對象不再被需要),智能指針對象會通過delete操作符或者指定deleter函數釋放動态對象。

第6 ~ 21行:為了示範智能指針所管理的動态對象的生命周期,我們設計了Fish類。Fish的構造及析構函數都會向控制台報告構造或析構的消息。

第23 ~ 25行:sayHello()函數接受一個智能指針對象f為參數,然後對f使用指向操作符通路Fish對象的sayHello()方法。請注意,f是一個智能指針對象,此處的參數傳遞為傳值;第24行的指向操作符事實上執行的是f對象的重載operator->()函數。

第27 ~ 29行:函數名重載的sayHello()接受Fish的引用f作為參數,然後執行f的sayHello()成員函數。

第32行:定義并建構了指向Fish對象的智能指針對象dora1,以動态Fish對象"Dora"的位址作為參數。該行代碼執行過程包含如下幾步。

•dora1是一個自動對象,在棧内為其配置設定空間;

•new Fish("Dora")在堆内配置設定對象空間并構造初始化,傳回“Dora魚”的位址;

•以new Fish("Dora")傳回的位址為參數,執行dora1的構造函數。該構造函數将動态對象的位址儲存在dora1内部,并将引用計數置為1,以表明該動态對象目前被1個智能指針對象所“引用”。

執行結果的第1行對應"Dora魚"的構造輸出。

第33行:該行代碼的執行包含如下幾步。

•tom1是一個自動對象,在棧内為其配置設定空間;

•make_shared<Fish>("Tom")函數建立并構造一個Fish類型的堆對象(以"Tom"為參數),然後構造并傳回一個指向該動态對象的shared_ptr<Fish>類型的智能指針;

•上述傳回的智能指針被拷貝構造給tom1。

注意 由于編譯器優化的原因,編譯器有可能會省掉先構造智能指針再拷貝構造智能指針的不必要步驟,而直接執行tom1的構造函數,以動态Fish對象的指針作為參數。

執行結果的第2行對應"Tom魚"的構造輸出。

第36行:将智能指針對象tom1傳值給sayHello()函數(第23行)的形參f,該函數對f應用指向操作符,執行“Tom魚”的sayHello()方法。可以想象,當f對象被拷貝構造時,“Tom魚”的引用計數将由1變2,因為此時tom1和f兩個智能指針都指向“Tom魚”;在sayHello()函數結束執行前,局部對象f被析構,“Tom魚”的引用計數則會由2變1。本行的輸出見執行結果的第4行。

第37行:從tom1拷貝構造tom2。“Tom魚”的引用計數将由1變2,因為智能指針tom1和tom2都指向“Tom魚”。

第38行:第27行的sayHello()函數接受一個Fish&作為參數,通過對tom2使用解引用操作符*,可以獲得Fish類型的對象。tom2的類型可以視為Fish*,*tom2的類型則為Fish。事實上,*tom2是通過執行tom2對象的operator*()操作符函數來實作“解引用”的功能的。本行的輸出見執行結果的第5行。

第41行:對智能指針dora1使用指向操作符,執行“Dora魚”的sayHello()方法。如前所述,該指向操作符事實上是通過dora1的operator->()操作符函數發揮作用的。本行的輸出見執行結果的第7行。

第42 ~ 43行:執行dora1對象的get()成員函數擷取智能指針内部的“原始”指針,然後通過原始指針執行“Dora魚”的sayHello()方法。成員操作符“.”證明,dora1是一個對象,而不是一個平凡的指針。相關輸出見執行結果的第8行。

到了main()函數的結尾,自動對象tom2的析構導緻“Tom魚”的引用計數由2變1、tom1的析構則進一步導緻引用計數由1變0,這表明“Tom魚”不再被任何智能指針所引用。在tom1的析構函數裡,delete操作符被執行,“Tom魚”動态對象被釋放。執行結果的第10行可見“Tom魚”的析構輸出。

同理,dora1的析構導緻“Dora魚”的釋放,執行結果的第11行可見“Dora魚”的析構輸出。

智能指針的使用簡化了動态對象的生命周期管理,程式員不必再手動釋放動态對象。當最後一個指向該動态對象的智能指針對象被析構時,該動态對象會被釋放。大多數情況下,由此所導緻的性能損失是可以接受的。

shared_ptr<Fish> p1(new Fish("1"));  //1号魚及其指針p1
auto p2 = make_shared<Fish>("2");    //2号魚及其指針p2
p2 = p1;       //p2與原引用對象解綁,改為綁定p1所指向的對象
p1.reset();    //p1與對象解綁
if (p1.get() == nullptr)   //解綁後的p1是空指針
     cout << "p1 is nullptr." << endl;
p1.reset(p2.get());        //p1與原對象解綁,改為綁定p2所指向的對象           

上述代碼進一步示範了shared_ptr的使用方法。

第3行:将p1指派給p2,這将導緻如下結果。

•p2與原對象解除綁定,本例中p2是指向原對象的唯一智能指針,原對象即2号魚被釋放;

•p2改為指向p1所指向的對象。

第4行:p1的reset()成員函數将解除p1與對象的綁定,解綁後,p1成為“空指針”。

第7行:p1.reset(p2.get())的執行導緻如下結果。

•p1與原綁定對象解綁;

•p1改為指向p2所指向的對象。

警告

• 智能指針僅适用于動态(堆)對象,不要對棧對象或者靜态對象建立智能指針,因為棧對象和靜态對象的生命周期是由編譯器自動管理的。

• 不要通過動态對象的原始指針建立多個互不相關的智能指針。

• 智能指針的get()方法所傳回的原始指針隻可使用,不可應用delete進行釋放。如果這樣做了,當智能指針析構時,會對同一個動态對象進行第2次釋放。

下述代碼示範了一些常見的智能指針的錯誤使用方法:

Fish* f = new Fish("Peter");
shared_ptr<Fish> p1(f);
auto p2 = p1;            //正确
shared_ptr<Fish> p3(f);  //錯誤           

第3行:正确,p2的出現僅會導緻動态對象引用計數的增加,當且僅當p2和p1都被析構時,“Peter魚”才會被釋放。

第4行:錯誤,智能指針p3與p1/p2互不相關,它會為“Peter魚”建立一個新的引用計數。本例中,p1(以及p2)會釋放“Peter魚”,p3也會釋放“Peter魚”,顯然,我們不可以殺死同一條魚兩次。

【C++】不能兩次殺死同一條魚 - 淺述shared_ptr智能指針的使用方法
Fish* p = p3.get();     //p3是個指向Fish對象的智能指針
delete p;               //錯誤           

第2行:錯誤,釋放p指針所指向的對象後,智能指針p3以及他的表兄弟們,還會再釋放一次。同理,不能兩次殺死同一條魚。

讀者如果對智能指針的工作機制感到疑惑,請仔細閱讀下一節 - 智能指針的實作。

普通的指針可以指向一個使用new []操作符建立的動态數組,智能指針也可以。

shared_ptr<float> a(new float[1024]);           

無論是new float,還是new float[1024],所得到的都是float*,也就是說,上述代碼中的智能指針a并不知曉其指向的是一個動态對象,還是由多個動态對象構成的數組。根據第8章的讨論,new []所傳回的指針必須通過delete []操作符進行釋放,顯然,上述智能指針a并不知道它所指向是一個動态數組,它隻能使用delete而不是delete []來釋放對象,這樣做有風險。

警告 當使用shared_ptr管理動态對象數組時,要麼指定類型為對象數組,要麼提供一個删除者(deleter)函數通過delete []釋放數組,該函數将在智能指針釋放對象時被調用。

下述C++代碼示範了確定智能指針安全釋放對象數組的方法。

//Project - SharedPtrArray
#include <iostream>
#include <memory>
using namespace std;

template <typename T>
void delete_array(T* p){
    cout << "delete_array" << endl;
    delete[] p;
}

class Fish{
public:
    ~Fish(){ cout << "Fish::Fish~()" << endl; }
};

int main(){
    shared_ptr<Fish[]> a(new Fish[4]);
    a = nullptr;   //a指針指向的數組被釋放

    shared_ptr<Fish> b(new Fish[2],delete_array<Fish>);
    b.reset();     //b指針指向的數組被釋放

    shared_ptr<float> c(new float[512],
        [](float*p){cout << "lambda function\n"; delete[] p;});
    *c = 4.4F;
    //c++;           //錯誤:智能指針不支援指針運算
    //c[1] = 99.2F;  //錯誤:智能指針不支援[]操作符
    cout << *c << endl;
    return 0;
}           

上述代碼的執行結果為:

Fish::Fish~()
Fish::Fish~()
Fish::Fish~()
Fish::Fish~()
delete_array
Fish::Fish~()
Fish::Fish~()
4.4
lambda function           

第18 ~ 19行:智能指針a的類型中的模闆參數被指定為Fish [],這相當于告知a對象,其所管理的是一個元素類型為Fish的動态數組。第19行給指針a指派為nullptr,将導緻a指針釋放對象數組。從執行結果的第1 ~ 4行可見,共有4次Fish對象的析構函數執行,這間接說明,對象數組的釋放是通過delete []操作符進行的。

第21 ~ 22行:智能指針b的構造函數的第2個參數為自定義的删除者函數。該函數是一個通用的模闆函數,其通過delete []操作符釋放對象數組。執行結果的第5 ~ 7行顯示,該函數成功執行,并析構了兩個Fish對象。

第24 ~ 25行:程式為智能指針c提供了一個匿名函數【C++ 11】▲作為删除者函數。執行結果的第9行對應該匿名函數的執行輸出。自動對象c在main()函數的結尾被釋放并引發了删除者函數的執行。

第27行:如注釋所言,智能指針不支援象普通指針那樣的指針運算。如果确實需要,隻能通過get()方法擷取原始指針後進行。

第28行:同樣地,智能指針也不支援普通指針那樣的[]操作符。

本案例節選自作者編寫的教材及配套實驗指導書。

《C++程式設計基礎及應用》(高等教育出版社,出版過程中)

《Python程式設計基礎及應用》,高等教育出版社

《Python程式設計基礎及應用實驗教程》,高等教育出版社

【C++】不能兩次殺死同一條魚 - 淺述shared_ptr智能指針的使用方法

高校教師同行如果期望索取樣書,教學支援資料,加群,請私信作者,聯系時請提供學校及個人姓名為盼,各高校在讀學生勿擾為謝。

青少年讀者們如果期望系統性地學習Python及C/C++程式設計語言,歡迎嘗試下述今日頭條(西瓜)免費視訊課程。

C/C++從入門到放棄(重慶大學現場版)

Python程式設計基礎及應用(重慶大學現場版)

【C++】不能兩次殺死同一條魚 - 淺述shared_ptr智能指針的使用方法

繼續閱讀