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

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

我們知道在 C++ 當中,類類型是一種由用戶自定義的數(shù)據類型。既然是數(shù)據類型,我們很自然地會希望在定義上和其他的內置類型有著相同的操作。回想一下,當我們在定義一個內置類型變量時,我們需要考慮以下幾種情況:

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

而為了實現(xiàn)這樣的功能,C++ 為類類型提供了構造函數(shù)、析構函數(shù)、拷貝構造函數(shù)、拷貝賦值運算符、移動構造函數(shù)和移動賦值運算符。其中類的拷貝控制成員包括了析構函數(shù)、拷貝構造函數(shù)、拷貝賦值運算符、移動構造函數(shù)和移動賦值運算符,而后兩者則是在 C++ 的新標準中引入的,它們?yōu)轭愄峁┝?“剪切” 操作。

析構函數(shù)

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

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

從定義方式可以看到,析構函數(shù)沒有返回值,沒有參數(shù)列表,且函數(shù)名固定為 “~類名”。由于沒有參數(shù)列表,因此析構函數(shù)不可以重載,一個類中只能有一個析構函數(shù)。

當一個對象被銷毀時,會自動調用其對應的析構函數(shù)。在析構函數(shù)中,首先會執(zhí)行函數(shù)體,然后按照成員初始化順序進行逆序銷毀。如果對象成員中有其他類的對象,則也會調用對應析構函數(shù)進行銷毀操作。這也就意味著析構函數(shù)本身并不直接銷毀對象成員。成員對象是在析構函數(shù)之后隱含的析構階段銷毀的,而析構函數(shù)體是作為成員銷毀步驟的一個前置操作。
一個對象被銷毀的時機主要有以下幾種:

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

當用戶沒有顯式地定義類的析構函數(shù),那么編譯器會自動為類生成默認的析構函數(shù)。默認的析構函數(shù)有兩種可能

  • 默認析構函數(shù)的函數(shù)體為空,什么都不操作
  • 若類的某個成員的析構函數(shù)被指明為 = delete 的,則這個類的默認析構函數(shù)也是 = delete 的。

拷貝構造函數(shù)

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

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

從定義上來看,拷貝構造函數(shù)的形參是常引用類型,沒有返回值。其中需要注意的是,拷貝構造函數(shù)的參數(shù)必須是引用類型,而不能為值類型。如果形參為值類型,則在調用拷貝構造函數(shù)時,需要將實參拷貝給形參,則會引發(fā)新一輪拷貝構造函數(shù)的調用,導致無限遞歸。將引用定義為 const 是因為拷貝構造函數(shù)不應當修改源對象的值,但這并非強制要求。當用戶沒有顯式定義拷貝構造函數(shù)時,而程序中又使用到了對象的拷貝功能,則編譯器會自動生成默認的拷貝構造函數(shù)。默認的拷貝構造函數(shù)只能實現(xiàn)淺拷貝操作。

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

string A("test");   //直接初始化,調用構造函數(shù)
string B(A);        //直接初始化,調用拷貝構造函數(shù)
string C = A;       //拷貝初始化,調用拷貝構造函數(shù)
string D = "test";  //拷貝初始化,調用拷貝構造函數(shù)
string E = string("test");  //拷貝初始化,調用拷貝構造函數(shù)

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

拷貝賦值運算符

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

class MyClass{
public:
    ...
    // 拷貝構造函數(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 來預防自賦值操作。同時為了保證運算符的實現(xiàn)是異常安全的,我們采用先將右值保存到一個臨時對象中,隨后釋放自身的成員對象,并完成拷貝操作。同樣的,如果沒有顯式地定義類的拷貝賦值函數(shù)而代碼又使用了拷貝賦值功能,那么編譯器將會自動生成默認的拷貝賦值運算符。

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

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

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

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

    這主要是因為當程序需要顯式定義析構函數(shù)時,往往意味著我們需要手動釋放資源。而使用默認拷貝構造函數(shù)則會導致“深淺拷貝問題”。

    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;
    }
    

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

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

  3. 如果一個類是可拷貝的,那么它應該是可移動的。但如果一個類是可移動的,它不一定是可拷貝的,例如 unique_ptr 或 IO 類

注:三/五法則并不是指有 3 條或 5 條法則,而是因為在 C++ 的早期標準中只有析構函數(shù)、拷貝構造函數(shù)和拷貝賦值運算符,這三者應當作為整體考慮,這稱之為 “C++ 三法則”。而 C++ 的新標準引入了移動構造函數(shù)和移動賦值運算符,將三法則擴充為五法則,后統(tǒng)一稱之為 “三/五法則” 。詳見《C++ 三法則

如何阻止對象的拷貝功能

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

在早期的 C++ 標準中,用戶可以通過將拷貝構造函數(shù)和拷貝賦值運算符的訪問權限設置為 private,而且對這兩個函數(shù)只聲明不定義。由于設置 private,因此當用戶代碼試圖拷貝類對象時,會產生編譯錯誤。若成員函數(shù)或友元函數(shù)試圖拷貝對象,則會因函數(shù)未定義而引發(fā)鏈接錯誤。

在 C++ 的新標準中引入了 = 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ù),來禁止某些特定的隱式類型轉換。例如一個針對 int 類型的 add 函數(shù),我們不希望當用戶傳遞 double 類型的實參時,會因為隱式類型轉換而損失精度,我們可以禁止 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ū)別

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

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

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

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

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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容