1. 程式人生 > >C++ 拷貝建構函式和賦值運算子

C++ 拷貝建構函式和賦值運算子

本文主要介紹了拷貝建構函式和賦值運算子的區別,以及在什麼時候呼叫拷貝建構函式、什麼情況下呼叫賦值運算子。最後,簡單的分析了下深拷貝和淺拷貝的問題。

拷貝建構函式和賦值運算子

在預設情況下(使用者沒有定義,但是也沒有顯式的刪除),編譯器會自動的隱式生成一個拷貝建構函式和賦值運算子。但使用者可以使用delete來指定不生成拷貝建構函式和賦值運算子,這樣的物件就不能通過值傳遞,也不能進行賦值運算。

class Person
{
public:

    Person(const Person& p) = delete;

    Person& operator=(const Person& p) = delete;

private:
    int age;
    string name;
};

上面的定義的類Person顯式的刪除了拷貝建構函式和賦值運算子,在需要呼叫拷貝建構函式或者賦值運算子的地方,會提示_無法呼叫該函式,它是已刪除的函式_。 還有一點需要注意的是,拷貝建構函式必須以引用的方式傳遞引數。這是因為,在值傳遞的方式傳遞給一個函式的時候,會呼叫拷貝建構函式生成函式的實參。如果拷貝建構函式的引數仍然是以值的方式,就會無限迴圈的呼叫下去,直到函式的棧溢位。

何時呼叫

拷貝建構函式和賦值運算子的行為比較相似,都是將一個物件的值複製給另一個物件;但是其結果卻有些不同,拷貝建構函式使用傳入物件的值生成一個新的物件的例項,而賦值運算子是將物件的值複製給一個已經存在的例項。這種區別從兩者的名字也可以很輕易的分辨出來,拷貝建構函式也是一種建構函式,那麼它的功能就是建立一個新的物件例項;賦值運算子是執行某種運算,將一個物件的值複製給另一個物件(已經存在的)。呼叫的是拷貝建構函式還是賦值運算子,主要是看是否有新的物件例項產生。如果產生了新的物件例項,那呼叫的就是拷貝建構函式;如果沒有,那就是對已有的物件賦值,呼叫的是賦值運算子

呼叫拷貝建構函式主要有以下場景:

  • 物件作為函式的引數,以值傳遞的方式傳給函式。 
  • 物件作為函式的返回值,以值的方式從函式返回
  • 使用一個物件給另一個物件初始化

程式碼如下:

class Person
{
public:
    Person(){}
    Person(const Person& p)
    {
        cout << "Copy Constructor" << endl;
    }

    Person& operator=(const Person& p)
    {
        cout << "Assign" << endl;
        return *this;
    }

private:
    int age;
    string name;
};

void f(Person p)
{
    return;
}

Person f1()
{
    Person p;
    return p;
}

int main()
{
    Person p;
    Person p1 = p;    // 1
    Person p2;
    p2 = p;           // 2
    f(p2);            // 3

    p2 = f1();        // 4

    Person p3 = f1(); // 5

    getchar();
    return 0;
}

上面程式碼中定義了一個類Person,顯式的定義了拷貝建構函式和賦值運算子。然後定義了兩個函式:f,以值的方式參傳入Person物件;f1,以值的方式返回Person物件。在main中模擬了5中場景,測試呼叫的是拷貝建構函式還是賦值運算子。執行結果如下:

分析如下:

  1. 這是雖然使用了"=",但是實際上使用物件p來建立一個新的物件p1。也就是產生了新的物件,所以呼叫的是拷貝建構函式。
  2. 首先宣告一個物件p2,然後使用賦值運算子"=",將p的值複製給p2,顯然是呼叫賦值運算子,為一個已經存在的物件賦值 。
  3. 以值傳遞的方式將物件p2傳入函式f內,呼叫拷貝建構函式構建一個函式f可用的實參。
  4. 這條語句拷貝建構函式和賦值運算子都呼叫了。函式f1以值的方式返回一個Person物件,在返回時會呼叫拷貝建構函式建立一個臨時物件tmp作為返回值;返回後呼叫賦值運算子將臨時物件tmp賦值給p2.
  5. 按照4的解釋,應該是首先呼叫拷貝建構函式建立臨時物件;然後再呼叫拷貝建構函式使用剛才建立的臨時物件建立新的物件p3,也就是會呼叫兩次拷貝建構函式。不過,編譯器也沒有那麼傻,應該是直接呼叫拷貝建構函式使用返回值建立了物件p3。

深拷貝、淺拷貝

說到拷貝建構函式,就不得不提深拷貝和淺拷貝。通常,預設生成的拷貝建構函式和賦值運算子,只是簡單的進行值的複製。例如:上面的Person類,欄位只有intstring兩種型別,這在拷貝或者賦值時進行值複製建立的出來的物件和源物件也是沒有任何關聯,對源物件的任何操作都不會影響到拷貝出來的物件。反之,假如Person有一個物件為int *,這時在拷貝時還只是進行值複製,那麼創建出來的Person物件的int *的值就和源物件的int *指向的是同一個位置。任何一個物件對該值的修改都會影響到另一個物件,這種情況就是淺拷貝。

深拷貝和淺拷貝主要是針對類中的指標動態分配的空間來說的,因為對於指標只是簡單的值複製並不能分割開兩個物件的關聯,任何一個物件對該指標的操作都會影響到另一個物件。這時候就需要提供自定義的深拷貝的拷貝建構函式,消除這種影響。通常的原則是:

  • 含有指標型別的成員或者有動態分配記憶體的成員都應該提供自定義的拷貝建構函式
  • 在提供拷貝建構函式的同時,還應該考慮實現自定義的賦值運算子

對於拷貝建構函式的實現要確保以下幾點:

  • 對於值型別的成員進行值複製
  • 對於指標和動態分配的空間,在拷貝中應重新分配分配空間
  • 對於基類,要呼叫基類合適的拷貝方法,完成基類的拷貝

總結

  • 拷貝建構函式和賦值運算子的行為比較相似,卻產生不同的結果;拷貝建構函式使用已有的物件建立一個新的物件,賦值運算子是將一個物件的值複製給另一個已存在的物件。區分是呼叫拷貝建構函式還是賦值運算子,主要是否有新的物件產生。
  • 關於深拷貝和淺拷貝。當類有指標成員或有動態分配空間,都應實現自定義的拷貝建構函式。提供了拷貝建構函式,最後也實現賦值運算子。