C++ 拷貝構造函數和賦值運算符

本文主要介紹了拷貝構造函數和賦值運算符的區別,以及在何時調用拷貝構造函數、什麼狀況下調用賦值運算符。最後,簡單的分析了下深拷貝和淺拷貝的問題。函數

拷貝構造函數和賦值運算符

在默認狀況下(用戶沒有定義,可是也沒有顯式的刪除),編譯器會自動的隱式生成一個拷貝構造函數和賦值運算符。但用戶可使用delete來指定不生成拷貝構造函數和賦值運算符,這樣的對象就不能經過值傳遞,也不能進行賦值運算。測試

class Person
{
public:

    Person(const Person& p) = delete;

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

private:
    int age;
    string name;
};

上面的定義的類Person顯式的刪除了拷貝構造函數和賦值運算符,在須要調用拷貝構造函數或者賦值運算符的地方,會提示_沒法調用該函數,它是已刪除的函數_。
還有一點須要注意的是,拷貝構造函數必須以引用的方式傳遞參數。這是由於,在值傳遞的方式傳遞給一個函數的時候,會調用拷貝構造函數生成函數的實參。若是拷貝構造函數的參數仍然是以值的方式,就會無限循環的調用下去,直到函數的棧溢出。this

什麼時候調用

拷貝構造函數和賦值運算符的行爲比較類似,都是將一個對象的值複製給另外一個對象;可是其結果卻有些不一樣,拷貝構造函數使用傳入對象的值生成一個新的對象的實例,而賦值運算符是將對象的值複製給一個已經存在的實例。這種區別從二者的名字也能夠很輕易的分辨出來,拷貝構造函數也是一種構造函數,那麼它的功能就是建立一個新的對象實例;賦值運算符是執行某種運算,將一個對象的值複製給另外一個對象(已經存在的)。調用的是拷貝構造函數仍是賦值運算符,主要是看是否有新的對象實例產生。若是產生了新的對象實例,那調用的就是拷貝構造函數;若是沒有,那就是對已有的對象賦值,調用的是賦值運算符指針

調用拷貝構造函數主要有如下場景:code

  • 對象做爲函數的參數,以值傳遞的方式傳給函數。 
  • 對象做爲函數的返回值,以值的方式從函數返回
  • 使用一個對象給另外一個對象初始化

代碼以下:對象

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中場景,測試調用的是拷貝構造函數仍是賦值運算符。執行結果以下:
blog

分析以下:內存

  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 *指向的是同一個位置。任何一個對象對該值的修改都會影響到另外一個對象,這種狀況就是淺拷貝。get

深拷貝和淺拷貝主要是針對類中的指針動態分配的空間來講的,由於對於指針只是簡單的值複製並不能分割開兩個對象的關聯,任何一個對象對該指針的操做都會影響到另外一個對象。這時候就須要提供自定義的深拷貝的拷貝構造函數,消除這種影響。一般的原則是:編譯器

  • 含有指針類型的成員或者有動態分配內存的成員都應該提供自定義的拷貝構造函數
  • 在提供拷貝構造函數的同時,還應該考慮實現自定義的賦值運算符

對於拷貝構造函數的實現要確保如下幾點:

  • 對於值類型的成員進行值複製
  • 對於指針和動態分配的空間,在拷貝中應從新分配分配空間
  • 對於基類,要調用基類合適的拷貝方法,完成基類的拷貝

總結

  • 拷貝構造函數和賦值運算符的行爲比較類似,卻產生不一樣的結果;拷貝構造函數使用已有的對象建立一個新的對象,賦值運算符是將一個對象的值複製給另外一個已存在的對象。區分是調用拷貝構造函數仍是賦值運算符,主要是否有新的對象產生。
  • 關於深拷貝和淺拷貝。當類有指針成員或有動態分配空間,都應實現自定義的拷貝構造函數。提供了拷貝構造函數,最後也實現賦值運算符。
相關文章
相關標籤/搜索