C++ 拷貝控制(一) — 析構(gòu)函數(shù)诗祸、拷貝構(gòu)造函數(shù)與拷貝賦值函數(shù)

什么是 C++ 的拷貝控制 ?

我們知道在 C++ 當(dāng)中轴总,類類型是一種由用戶自定義的數(shù)據(jù)類型直颅。既然是數(shù)據(jù)類型,我們很自然地會希望在定義上和其他的內(nèi)置類型有著相同的操作怀樟」Τィ回想一下,當(dāng)我們在定義一個內(nèi)置類型變量時往堡,我們需要考慮以下幾種情況:

// 內(nèi)置類型
{
    int a = 10; //定義一個 int 變量并初始化
    int b = a;  //使用已定義好的變量 a 初始化變量 b
    int c;      //定義一個變量 c 并初始化為默認(rèn)值
    c = a;      //將 a 的值賦給 c
}// 離開作用域后將局部變量 a械荷、b、c 銷毀

而為了實現(xiàn)這樣的功能虑灰,C++ 為類類型提供了構(gòu)造函數(shù)吨瞎、析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)穆咐、拷貝賦值運算符颤诀、移動構(gòu)造函數(shù)和移動賦值運算符。其中類的拷貝控制成員包括了析構(gòu)函數(shù)对湃、拷貝構(gòu)造函數(shù)崖叫、拷貝賦值運算符、移動構(gòu)造函數(shù)和移動賦值運算符熟尉,而后兩者則是在 C++ 的新標(biāo)準(zhǔn)中引入的归露,它們?yōu)轭愄峁┝?“剪切” 操作。

析構(gòu)函數(shù)

析構(gòu)函數(shù)的定義方式如下:

class MyClass{
public:
    ...
    //析構(gòu)函數(shù)
    ~MyClass(){
        cout << "MyClass Deconstructor" << endl;
    }   
    ...
};

從定義方式可以看到斤儿,析構(gòu)函數(shù)沒有返回值剧包,沒有參數(shù)列表,且函數(shù)名固定為 “~類名”往果。由于沒有參數(shù)列表疆液,因此析構(gòu)函數(shù)不可以重載,一個類中只能有一個析構(gòu)函數(shù)陕贮。

當(dāng)一個對象被銷毀時堕油,會自動調(diào)用其對應(yīng)的析構(gòu)函數(shù)。在析構(gòu)函數(shù)中,首先會執(zhí)行函數(shù)體掉缺,然后按照成員初始化順序進行逆序銷毀卜录。如果對象成員中有其他類的對象,則也會調(diào)用對應(yīng)析構(gòu)函數(shù)進行銷毀操作眶明。這也就意味著析構(gòu)函數(shù)本身并不直接銷毀對象成員艰毒。成員對象是在析構(gòu)函數(shù)之后隱含的析構(gòu)階段銷毀的,而析構(gòu)函數(shù)體是作為成員銷毀步驟的一個前置操作搜囱。
一個對象被銷毀的時機主要有以下幾種:

  • 局部對象離開其作用域時會被銷毀
  • 存放對象的容器被銷毀丑瞧,容器中的對象都會被銷毀
  • 對動態(tài)分配內(nèi)存的對象指針執(zhí)行 delete 操作時,所指向的對象會被銷毀
  • 對于臨時對象蜀肘,當(dāng)創(chuàng)建它的完整表達式結(jié)束的時候銷毀

當(dāng)用戶沒有顯式地定義類的析構(gòu)函數(shù)绊汹,那么編譯器會自動為類生成默認(rèn)的析構(gòu)函數(shù)。默認(rèn)的析構(gòu)函數(shù)有兩種可能

  • 默認(rèn)析構(gòu)函數(shù)的函數(shù)體為空扮宠,什么都不操作
  • 若類的某個成員的析構(gòu)函數(shù)被指明為 = delete 的西乖,則這個類的默認(rèn)析構(gòu)函數(shù)也是 = delete 的。

拷貝構(gòu)造函數(shù)

拷貝構(gòu)造函數(shù)的函數(shù)定義如下

class MyClass{
public:
    ...
    // 拷貝構(gòu)造函數(shù)
    MyClass(const MyClass& obj):var(obj.var){
        cout << "MyClass Copy Constructor" << endl;
    }   
    ...
private:
    int var;
};

從定義上來看涵卵,拷貝構(gòu)造函數(shù)的形參是常引用類型浴栽,沒有返回值。其中需要注意的是轿偎,拷貝構(gòu)造函數(shù)的參數(shù)必須是引用類型典鸡,而不能為值類型。如果形參為值類型坏晦,則在調(diào)用拷貝構(gòu)造函數(shù)時萝玷,需要將實參拷貝給形參,則會引發(fā)新一輪拷貝構(gòu)造函數(shù)的調(diào)用昆婿,導(dǎo)致無限遞歸球碉。將引用定義為 const 是因為拷貝構(gòu)造函數(shù)不應(yīng)當(dāng)修改源對象的值,但這并非強制要求仓蛆。當(dāng)用戶沒有顯式定義拷貝構(gòu)造函數(shù)時睁冬,而程序中又使用到了對象的拷貝功能,則編譯器會自動生成默認(rèn)的拷貝構(gòu)造函數(shù)看疙。默認(rèn)的拷貝構(gòu)造函數(shù)只能實現(xiàn)淺拷貝操作豆拨。

由于拷貝構(gòu)造操作常常會被隱式調(diào)用,因此拷貝構(gòu)造函數(shù)通常不聲明為 explicit能庆。例如:

string A("test");   //直接初始化施禾,調(diào)用構(gòu)造函數(shù)
string B(A);        //直接初始化,調(diào)用拷貝構(gòu)造函數(shù)
string C = A;       //拷貝初始化搁胆,調(diào)用拷貝構(gòu)造函數(shù)
string D = "test";  //拷貝初始化弥搞,調(diào)用拷貝構(gòu)造函數(shù)
string E = string("test");  //拷貝初始化邮绿,調(diào)用拷貝構(gòu)造函數(shù)

由于編譯器優(yōu)化的原因,像 string D = "test" 通常會被優(yōu)化為 string D("test")攀例,進而提高執(zhí)行效率

拷貝賦值運算符

拷貝構(gòu)造運算符的函數(shù)定義如下

class MyClass{
public:
    ...
    // 拷貝構(gòu)造函數(shù)
    MyClass& operator=(const MyClass& obj){
        cout << "MyClass Copy Assignment" << endl;
        if(this != &obj){
            auto newp = new string(*obj.p_str);
            delete p_str;
            p_str = newp;
        }
        return *this;
    }   
    ...
private:
    string *p_str;
};

從定義上看船逮,拷貝賦值運算符是一個返回對象的引用,參數(shù)為對象的常引用的運算符函數(shù)粤铭。在實現(xiàn)的過程中傻唾,我們利用 this != obj 來預(yù)防自賦值操作。同時為了保證運算符的實現(xiàn)是異常安全的承耿,我們采用先將右值保存到一個臨時對象中,隨后釋放自身的成員對象伪煤,并完成拷貝操作加袋。同樣的,如果沒有顯式地定義類的拷貝賦值函數(shù)而代碼又使用了拷貝賦值功能抱既,那么編譯器將會自動生成默認(rèn)的拷貝賦值運算符。

在定義拷貝賦值運算符的時候,有三個需要注意的地方:

  • 當(dāng)使用一個對象對自身進行賦值時冶共,賦值運算符依然要保證有正確的行為闽烙。
  • 一個定義良好的拷貝賦值運算符應(yīng)當(dāng)是異常安全的,即當(dāng)異常發(fā)生時捷泞,能夠使左側(cè)運算對象處于一種有意義的狀態(tài)
  • 大多數(shù)的拷貝賦值運算符組合了析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù)的工作足删。

什么情況下,需要程序員手動實現(xiàn)拷貝構(gòu)造函數(shù)和拷貝賦值運算符(三/五法則)

  1. 當(dāng)類需要實現(xiàn)析構(gòu)函數(shù)時锁右,那么往往也需要實現(xiàn)拷貝構(gòu)造函數(shù)和拷貝賦值運算符失受。而實現(xiàn)了拷貝構(gòu)造函數(shù)和拷貝賦值運算符的類卻不一定要顯式定義析構(gòu)函數(shù)
    【注:基類的析構(gòu)函數(shù)是個例外,不遵循該原則】

    這主要是因為當(dāng)程序需要顯式定義析構(gòu)函數(shù)時咏瑟,往往意味著我們需要手動釋放資源拂到。而使用默認(rèn)拷貝構(gòu)造函數(shù)則會導(dǎo)致“深淺拷貝問題”。

    class A{
    public:
        A(string const s = ""):ps(new string(s)){}
        ~A(){delete ps;}
        void display(void){
            cout << *ps << endl;
        }
    private:
        string *ps;
    };
    int main(void){
        A* a = new A("test");
        A b(*a);
        A c;
        c = *a;
        delete a;
        b.display(); //對象 b 試圖訪問已被刪除的對象 a 中的 ps
        c.display(); //對象 c 試圖訪問已被刪除的對象 a 中的 ps
        return 0;
    }
    

    由于沒有顯式定義拷貝構(gòu)造函數(shù)和拷貝賦值運算符码泞,因此編譯器生成默認(rèn)拷貝構(gòu)造函數(shù)和拷貝賦值運算符兄旬,它們僅僅只拷貝了 a.ps 的值,但并沒有拷貝 a.ps 所指向的對象余寥。因此领铐,a.ps, b.ps, c.ps 均指向同一對象,在 a.ps 所指向的對象被釋放后劈狐,b 和 c 又試圖去訪問它罐孝,這種操作的后果是未定義的。

  2. 如果定義了拷貝構(gòu)造函數(shù)肥缔,那么通常也要定義拷貝賦值運算符莲兢;反之同理

  3. 如果一個類是可拷貝的,那么它應(yīng)該是可移動的。但如果一個類是可移動的改艇,它不一定是可拷貝的收班,例如 unique_ptr 或 IO 類

注:三/五法則并不是指有 3 條或 5 條法則,而是因為在 C++ 的早期標(biāo)準(zhǔn)中只有析構(gòu)函數(shù)谒兄、拷貝構(gòu)造函數(shù)和拷貝賦值運算符摔桦,這三者應(yīng)當(dāng)作為整體考慮,這稱之為 “C++ 三法則”承疲。而 C++ 的新標(biāo)準(zhǔn)引入了移動構(gòu)造函數(shù)和移動賦值運算符邻耕,將三法則擴充為五法則,后統(tǒng)一稱之為 “三/五法則” 燕鸽。詳見《C++ 三法則

如何阻止對象的拷貝功能

對于某些類對象而言兄世,比如 IO 類或者包含 unique_ptr 成員的類對象,我們不能為其提供拷貝操作啊研。即使我們不去實現(xiàn)拷貝構(gòu)造函數(shù)和拷貝賦值運算符御滩,編譯器也會自動生成默認(rèn)的拷貝構(gòu)造函數(shù)和拷貝賦值運算符。我們先來看看 C++ 的新舊標(biāo)準(zhǔn)當(dāng)中是如何解決這個問題的党远。

在早期的 C++ 標(biāo)準(zhǔn)中削解,用戶可以通過將拷貝構(gòu)造函數(shù)和拷貝賦值運算符的訪問權(quán)限設(shè)置為 private,而且對這兩個函數(shù)只聲明不定義沟娱。由于設(shè)置 private氛驮,因此當(dāng)用戶代碼試圖拷貝類對象時,會產(chǎn)生編譯錯誤济似。若成員函數(shù)或友元函數(shù)試圖拷貝對象柳爽,則會因函數(shù)未定義而引發(fā)鏈接錯誤。

在 C++ 的新標(biāo)準(zhǔn)中引入了 = delete 來表明顯式地禁止使用某個函數(shù)碱屁。具體的用法如下:

struct NoCopy{
    NoCopy();
    NoCopy(const NoCopy&) = delete;
    NoCopy& operator=(const NoCopy&) = delete;
    ~NoCopy() = default;
};
NoCopy::NoCopy() = default;
int main(void){
    NoCopy a;
    NoCopy b(a);    //error: use of deleted function 'NoCopy::NoCopy(const NoCopy&)'
    NoCopy c;
    c = a;          //error: use of deleted function 'NoCopy& NoCopy::operator=(const NoCopy&)'
    return 0;
}

顯然磷脯,引入 = delete 使得為了特定類類型提供禁止拷貝功能變得更加簡單。不僅如此娩脾,= delete 還可以修飾普通函數(shù)赵誓,來禁止某些特定的隱式類型轉(zhuǎn)換。例如一個針對 int 類型的 add 函數(shù)柿赊,我們不希望當(dāng)用戶傳遞 double 類型的實參時俩功,會因為隱式類型轉(zhuǎn)換而損失精度,我們可以禁止 add 的重載版本來實現(xiàn)這個功能:

int add(int const a, int const b){
    return a + b;
}
int add(double const a, double const b) = delete;
int main(void){
    cout << add(3,4) << endl;
    cout << add(3.0,4.5) << endl;   //error: use of deleted function 'double add(double, double)'|
    return 0;
}

= delete 和 = default 的區(qū)別

從前面的描述當(dāng)中碰声,我們可以看到 = delete 和 = default 在用法上的相似性诡蜓,接下來我們來看一看它們二者之間的區(qū)別

  • 理論上所有函數(shù)都可以指定為 = delete,而只有類的特殊成員函數(shù) (構(gòu)造函數(shù)胰挑、析構(gòu)函數(shù)蔓罚、拷貝構(gòu)造函數(shù)和拷貝賦值運算符) 才能指定為 = default

  • = default 可以在類內(nèi)(inline)聲明椿肩,也可以在類外(out of line) 聲明,而= delete 必須在函數(shù)的首次聲明時指定豺谈,這也就意味著 = delete 只能在類內(nèi)聲明

    struct A{
        A();
        A(const A&) = delete; //= delete 必須在函數(shù)首次聲明時指定
        A& operator=(const A&) = delete;
    };
    A::A() = default;         //= default 可以在類外聲明
    

注意:理論上所有函數(shù)都可以指定為 delete 的郑象,但通常情況下不能刪除析構(gòu)函數(shù)。如果析構(gòu)函數(shù)被刪除茬末,則無法銷毀此類型對象厂榛。而對于刪除了析構(gòu)函數(shù)的類,或者類成員中包含了刪除析構(gòu)函數(shù)的類的對象丽惭,則編譯器會禁止定義該類型的對象或創(chuàng)建該類型的臨時對象击奶。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市责掏,隨后出現(xiàn)的幾起案子正歼,更是在濱河造成了極大的恐慌,老刑警劉巖拷橘,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異喜爷,居然都是意外死亡冗疮,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門檩帐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來术幔,“玉大人,你說我怎么就攤上這事湃密∽缣簦” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵泛源,是天一觀的道長拔妥。 經(jīng)常有香客問我,道長达箍,這世上最難降的妖魔是什么没龙? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮缎玫,結(jié)果婚禮上硬纤,老公的妹妹穿的比我還像新娘。我一直安慰自己赃磨,他們只是感情好筝家,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著邻辉,像睡著了一般溪王。 火紅的嫁衣襯著肌膚如雪腮鞍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天在扰,我揣著相機與錄音缕减,去河邊找鬼。 笑死芒珠,一個胖子當(dāng)著我的面吹牛桥狡,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播皱卓,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼裹芝,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了娜汁?” 一聲冷哼從身側(cè)響起嫂易,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎掐禁,沒想到半個月后怜械,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡傅事,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年缕允,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蹭越。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡障本,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出响鹃,到底是詐尸還是另有隱情驾霜,我是刑警寧澤,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布买置,位于F島的核電站粪糙,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏忿项。R本人自食惡果不足惜猜旬,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望倦卖。 院中可真熱鬧洒擦,春花似錦、人聲如沸怕膛。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽褐捻。三九已至掸茅,卻和暖如春椅邓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背昧狮。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工景馁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人逗鸣。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓合住,卻偏偏與公主長得像,于是被迫代替她去往敵國和親撒璧。 傳聞我的和親對象是個殘疾皇子透葛,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355