天天看點

(非常重要)C++中複制構造函數與重載指派操作符總結

前言

這篇文章将對C++中複制構造函數和重載指派操作符進行總結,包括以下内容:

  1. 複制構造函數和重載指派操作符的定義;
  2. 複制構造函數和重載指派操作符的調用時機;
  3. 複制構造函數和重載指派操作符的實作要點;
  4. 複制構造函數的一些細節。

複制構造函數和重載指派操作符的定義

我們都知道,在C++中建立一個類,這個類中肯定會包括構造函數、析構函數、複制構造函數和重載指派操作;即使在你沒有明确定義的情況下,編譯器也會給你生成這樣的四個函數。例如以下類:

class CTest
{
public:
     CTest();
     ~CTest();

     CTest(const CTest &);
     CTest& operator=(const CTest &);
};
           

對于構造函數和析構函數不是今天總結的重點,今天的重點是複制構造函數和重載指派操作。類的複制構造函數原型如下:

一般來說,如果我們沒有編寫複制構造函數,那麼編譯器會自動地替每一個類建立一個複制構造函數(也叫隐式複制構造函數);相反的,如果我們編寫了一個複制構造函數(顯式的複制構造函數),那麼編譯器就不會建立它。

類的重載指派操作符的原型如下:

重載指派操作符是一個特别的指派運算符,通常是用來把已存在的對象指定給其它相同類型的對象。它是一個特别的成員函數,如果我們沒有定義這個成員函數,那麼編譯器會自動地産生這個成員函數。編譯器産生的代碼是以單一成員進行對象複制的動作。

總結了複制構造函數和重載指派操作符的定義,隻是讓我們了解了它們,而沒有真正的深入它們。接下來,再仔細的總結一下它們的調用時機。關于它們的調用時機,我一直都沒有真正的明白過,是以這裡一定要好好的總結明白了。

複制構造函數和重載指派操作符的調用時機

對複制構造函數和重載指派操作符的調用總是發生在不經意間,它們不是經過我們顯式的去調用就被執行了。對于這種隐式調用的地方一定要多注意了,這也一般是有陷阱的地方。現在我就用實際的例子來進行驗證;例子如下:

#include <iostream>
using namespace std;

class CTest
{
public:
     CTest(){}
     ~CTest(){}

     CTest(const CTest &test)
     {
          cout<<"copy constructor."<<endl;
     }

     void operator=(const CTest &test)
     {
          cout<<"operator="<<endl;
     }

     void Test(CTest test)
     {}

     CTest Test2()
     {
          CTest a;
          return a;
     }

     void Test3(CTest &test)
     {}

     CTest &Test4()
     {
          CTest *pA = new CTest;
          return *pA;
     }
};

int main()
{
     CTest obj;

     CTest obj1(obj); // 調用複制構造函數

     obj1 = obj; // 調用重載指派操作符

     /* 傳參的過程中,要調用一次複制構造函數
     * obj1入棧時會調用複制構造函數建立一個臨時對象,與函數内的局部變量具有相同的作用域
     */
     obj.Test(obj1);

     /* 函數傳回值時,調用複制構造函數;将傳回值指派給obj2時,調用重載指派操作符
     * 函數傳回值時,也會構造一個臨時對象;調用複制構造函數将傳回值複制到臨時對象上
     */
     CTest obj2;
     obj2 = obj.Test2();

     obj2.Test3(obj); // 參數是引用,沒有調用複制構造函數

     CTest obj3;
     obj2.Test4(); // 傳回值是引用,沒有調用複制構造函數

     return 0;
}
           

在代碼中都加入了注釋,這裡就不再做詳細的說明了。再次總結一下,如果對象在聲明的同時将另一個已存在的對象賦給它,就會調用複制構造函數;如果對象已經存在了,然後再将另一個已存在的對象賦給它,調用的就是重載指派運算符了。這條規則很适用,希望大家能記住。

複制構造函數和重載指派操作符的實作要點

在一般的情況下,編譯器給我們生成的預設的複制構造函數和重載指派操作符就已經夠用了;但是在一些特别的時候,需要我們手動去實作自己的複制構造函數。

我們都知道,預設的複制構造函數和指派運算符進行的都是”shallow copy”,隻是簡單地複制字段,是以如果對象中含有動态配置設定的記憶體,就需要我們自己重寫複制構造函數或者重載指派運算符來實作”deep copy”,確定資料的完整性和安全性。這也就是大家常常說的深拷貝與淺拷貝的問題。下面我就提供一個比較簡單的例子來說明一下:

#include <iostream>
using namespace std;

const int MAXSIZE = 260;

class CTest
{
public:
     CTest(wchar_t *pInitValue)
     {
          // Here, I malloc the memory
          pValue = new wchar_t[MAXSIZE];
          memset(pValue, 0, sizeof(wchar_t) * MAXSIZE);
          wcscpy_s(pValue, MAXSIZE, pInitValue);
     }

     ~CTest()
     {
          if (pValue)
          {
               delete[] pValue; //finalseabiscuit指出,謝謝。2014.7.24
               pValue = NULL;
          }
     }

     CTest(const CTest &test)
     {
          // Malloc the new memory for the pValue
          pValue = new wchar_t[MAXSIZE];
          memset(pValue, 0, sizeof(wchar_t) * MAXSIZE);
          wcscpy_s(pValue, MAXSIZE, test.pValue);
     }

     CTest& operator=(const CTest &test)
     {
          // This is very important, please remember
          if (this == &test)
          {
               return *this;
          }

          // Please delete the memory, this maybe cause the memory leak
          if (pValue)
          {
               delete[] pValue; // 方恒剛指出的問題。非常感謝 2014.3.15
          }

          // Malloc the new memory for the pValue
          pValue = new wchar_t[MAXSIZE];
          memset(pValue, 0, sizeof(wchar_t) * MAXSIZE);
          wcscpy_s(pValue, MAXSIZE, test.pValue);
          return *this;
     }

     void Print()
     {
          wcout<<pValue<<endl;
     }

private:
     wchar_t *pValue; // The pointer points the memory
};

int main()
{
     CTest obj(L"obj");
     obj.Print();

     CTest obj2(L"obj2");
     obj2.Print();
     obj2 = obj;
     obj2.Print();

     obj2 = obj2;
     obj2.Print();

     return 0;
}
           

特别是在實作重載指派構造函數時需要多多的注意,在代碼中我也添加了注釋,大家可以認真的閱讀一下代碼,然後就懂了,如果不懂的就可以留言問我;當然了,如果我哪裡了解錯了,也希望大家能給我提出,我們共同進步。

複制構造函數的一些細節

  1. 以下哪些是複制構造函數
    X::X(const X&);   
    X::X(X);   
    X::X(X&, int a=1);   
    X::X(X&, int a=1, int b=2);
               
    這些細節問題在這裡也說一說,我也是從别人的部落格裡看到的,這裡自己也總結一下。對于一個類X, 如果一個構造函數的第一個參數是下列之一:
    a) X&
    b) const X&
    c) volatile X&
    d) const volatile X&
               
    且沒有其他參數或其他參數都有預設值,那麼這個函數是拷貝構造函數。
    X::X(const X&);  //是拷貝構造函數   
    X::X(X&, int=1); //是拷貝構造函數  
    X::X(X&, int a=1, int b=2); //當然也是拷貝構造函數
               
  2. 類中可以存在超過一個拷貝構造函數
    class X 
    { 
    public:       
      X(const X&);      // const 的拷貝構造
      X(X&);            // 非const的拷貝構造
    };
               
    注意,如果一個類中隻存在一個參數為 X& 的拷貝構造函數,那麼就不能使用const X或volatile X的對象實行拷貝初始化。如果一個類中沒有定義拷貝構造函數,那麼編譯器會自動産生一個預設的拷貝構造函數。這個預設的參數可能為 X::X(const X&)或 X::X(X&),由編譯器根據上下文決定選擇哪一個。在我的Visual Studio 2012中,當定義了多個複制構造函數以後,編譯器就會有warning,但是程式還能正确運作。

總結

這篇文章對複制構造函數和重載指派操作符進行了一些總結,重點是在複制構造函數與重載指派操作符的調用時機上;對于大家喜歡總結的深拷貝與淺拷貝問題,我沒有用過多的文字進行說明,我認為上面的代碼就足以說明問題了。最後自己糾結已久的問題也就這樣總結了,自己也徹底的明白了。

2014年2月21日 于大連,東軟。

繼續閱讀