C++中的拷貝構造函數和賦值函數

    C++的拷貝構造函數和賦值函數是兩個特別比較讓人混淆的概念,在使用中也經常容易出錯,在這裏我把C++的拷貝構造函數和賦值函數總結下。我從以下幾個方面來總結:

     1、什麼是拷貝構造函數和賦值函數,二者的區別

     2、C++拷貝構造函數和賦值函數的形式 ,爲什麼是拷貝構造函數式這種形式

     3、什麼是默認拷貝構造函數和默認賦值函數

     4、什麼是淺拷貝和深度拷貝

     5、拷貝構造函數和賦值函數使用中應該注意的問題

  

一、什麼是拷貝構造函數和賦值函數,二者區別

       首先,拷貝構造函數從名字看它是個構造函數,又加了個定語"拷貝",總結來說,拷貝構造函數是一種特殊的構造函數,它是用一個已經存在的類的對象去創建該類的一個新對象。賦值函數指對於兩個已經存在的類對象,用其中一個類對象對另外一個類對象進行賦值操作,拷貝構造函數和賦值函數非常容易混淆。拷貝構造函數是在對象被創建時調用的,而賦值函數只能被已經存在了的對象調用。

      請看下面例子:

       String a(“hello”);
  String b(“world”);
  String c = a; // 雖然用了“=”,但是調用了拷貝構造函數,最好寫成 c(a);
  c = b; // 調用了賦值函數
  本例中第三個語句的風格較差,宜改寫成String c(a) 以區別於第四個語句。

二、C++拷貝構造函數和賦值函數的形式 ,爲什麼是拷貝構造函數式這種形式

        對於Class A的拷貝構造函數的一般形式爲A (A const &a),思考下爲什麼是這種形式,如果只是死記硬背記住了那沒什麼意思。

      首先因爲拷貝構造函數是一種特殊的構造函數,既然是構造函數,函數名稱必須和類名一樣,且沒有返回值。那爲什麼參數必須是引用類型呢?如果不加引用,A(A   a)會怎麼樣?

      如果你嘗試會發現編譯通過不了。爲什麼編譯通過不了,編譯器爲什麼會報錯,試想如果你這樣寫

      Class A

     {

           public:

               A(A  a); // 拷貝構造函數

               A(int value)  // 一般構造函數

               {

                    val = value;

               }

               ......

          private:

           int val;

     };

    調用應該是是這樣:

          A a1(7); // 調用一般構造函數

          A b(a1);// 調用拷貝構造函數

   注意:在調用拷貝構造函數時,會把a1的副本a1_bak傳給形參a,那a1_bak又是如何產生的,“拷貝構造函數”,那拷貝構造函數呢?就是它本身!A(Aa)。這就是用需要調用自身的拷貝構造函數時又需要調用自身的拷貝構造函數,這樣就會陷入無窮遞歸!所以編譯器認爲這樣是不合法的!

     爲什麼要加入const,其實不加也可以,加了就是read-only,防止在拷貝構造函數中修改源對象的內容。

    賦值函數形式爲:

    A & A::operator =(const A &other)

    實際是運算符的重載,爲什麼參數和返回值類型是引用,這裏是僅僅爲了提高效率,如果不是引用類型也沒什麼問題,僅僅效率差些。

   看String類的拷貝構造函數和賦值函數
  // 拷貝構造函數
  String::String(const String &other)
  {
  // 允許操作other 的私有成員m_data
  int length = strlen(other.m_data);
  m_data = new char[length+1];
  strcpy(m_data, other.m_data);
  }
  // 賦值函數
  String & String::operator =(const String &other)
  {
  // (1) 檢查自賦值
  if(this == &other)
  return *this;
  // (2) 釋放原有的內存資源
  delete [] m_data;
  // (3)分配新的內存資源,並複製內容
  int length = strlen(other.m_data);
  m_data = new char[length+1];
  strcpy(m_data, other.m_data);
  // (4)返回本對象的引用
  return *this;
  }

      類String 拷貝構造函數與普通構造函數的區別是:在函數入口處無需與NULL 進行比較,這是因爲“引用”不可能是NULL,而“指針”可以爲NULL。類String 的賦值函數比構造函數複雜得多,分四步實現:
  (1)第一步,檢查自賦值。你可能會認爲多此一舉,難道有人會愚蠢到寫出 a = a 這樣的自賦值語句!的確不會。但是間接的自賦值仍有可能出現,例如
  // 內容自賦值
  b = a;
  …
  c = b;
  …
  a = c;
  // 地址自賦值
  b = &a;
  …
  a = *b;
  也許有人會說:“即使出現自賦值,我也可以不理睬,大不了化點時間讓對象複製自己而已,反正不會出錯!”他真的說錯了。看看第二步的delete,自殺後還能複製自己嗎?所以,如果發現自賦值,應該馬上終止函數。注意不要將檢查自賦值的if 語句
  if(this == &other)
  錯寫成爲
  if( *this == other)
  (2)第二步,用delete 釋放原有的內存資源。如果現在不釋放,以後就沒機會了,將造成內存泄露。
  (3)第三步,分配新的內存資源,並複製字符串。注意函數strlen 返回的是有效字符串長度,不包含結束符‘\0’。函數strcpy 則連‘\0’一起復制。
  (4)第四步,返回本對象的引用,目的是爲了實現象 a = b = c 這樣的鏈式表達。注意不要將 return *this 錯寫成 return this 。那麼能否寫成return other 呢?效果不是一樣嗎?不可以!因爲我們不知道參數other 的生命期。有可能other 是個臨時對象,在賦值結束後它馬上消失,那麼return other 返回的將是垃圾。


三、什麼是默認拷貝構造函數和默認賦值函數

      默認拷貝構造函數和默認賦值函數顧名思義就是當user沒有顯式定義拷貝構造函數和賦值函數時,編譯器隱式定義的拷貝構造函數和賦值函數。

      默認拷貝構造函數和默認賦值函數都是按照“位”拷貝來實現的,即對類中的對象所有的成員逐一拷貝,如果類中還嵌套着子類,那麼拷貝時按照子類的拷貝構造函數執行(子類的默認拷貝構造函數,賦值函數或者user定義的拷貝構造函數,賦值函數)。

      這樣就會有一個問題,如果類中的成員有指針,那麼默認的拷貝構造函數或者默認的賦值函數將會是對指針進行拷貝,而並非對指針的內容進行拷貝。借用網上看到的一個例子:以類String 的兩個對象a,b 爲例,假設a.m_data 的內容爲“hello”,b.m_data 的內容爲“world”。現將a 賦給b,缺省賦值函數的“位拷貝”意味着執行b.m_data = a.m_data。這將造成三個錯誤:一是b.m_data 原有的內存沒被釋放,造成內存泄露;二是b.m_data 和a.m_data 指向同一塊內存,a 或b 任何一方變動都會影響另一方;三是在對象被析構時,m_data 被釋放了兩次。

     所以對於class中的成員有指針的情況,需要自己定義拷貝構造函數和賦值函數,不能用編譯器默認的拷貝構造函數和賦值函數。

四、什麼是淺拷貝和深度拷貝

         所謂淺拷貝就是對Class中的成員賦值,不進行堆分配,默認拷貝構造函數屬於淺拷貝,深度拷貝即對類中成員有指針的情況進行堆分配。如果對類中成員有指針的情況不採取深度拷貝,可能會出現前面說過的默認拷貝構造函數出現的問題。

五、拷貝構造函數和賦值函數使用中應該注意的問題

   

     1、默認的拷貝構造函數沒有處理靜態數據成員

          以下是我在網上找到的一個例子:

class Rect  

{  
public:  
     Rect()      // 構造函數,計數器加1  
     {  
          count++;  
     }  
     ~Rect()     // 析構函數,計數器減1  
     {  
            count--;  
     }  
     static int getCount()       // 返回計數器的值  
     {  
            return count;  
     }  

private:  
      int width;  
      int height;  
      static int count;       // 一靜態成員做爲計數器 
}; 
 
int Rect::count = 0;        // 初始化計數器 
 
int main() 

    Rect rect1; 
    cout<<"The count of Rect: "<<Rect::getCount()<<endl; 
 
    Rect rect2(rect1);   // 使用rect1複製rect2,此時應該有兩個對象 
     cout<<"The count of Rect: "<<Rect::getCount()<<endl; 
 
    return 0; 

運行後兩次的輸出結果都是1。

修改後的class爲:

class Rect 

public: 
    Rect()      // 構造函數,計數器加1 
    { 
        count++; 
    } 
    Rect(const Rect& r)   // 拷貝構造函數 
    { 
        width = r.width; 
        height = r.height; 
        count++;          // 計數器加1 
    } 
    ~Rect()     // 析構函數,計數器減1 
    { 
        count--; 
    } 
    static int getCount()   // 返回計數器的值 
    { 
        return count; 
    } 
private: 
    int width; 
    int height; 
    static int count;       // 一靜態成員做爲計數器 
}; 

2、防止默認拷貝發生

        一個小技巧可以防止按值傳遞——聲明一個私有拷貝構造函數。甚至不必去定義這個拷貝構造函數,這樣因爲拷貝構造函數是私有的,如果用戶試圖按值傳遞或函數返回該類對象,將得到一個編譯錯誤,從而可以避免按值傳遞或返回對象。

3、拷貝構造函數形式

     對於一個類X, 如果一個構造函數的第一個參數是下列之一:
a) X&
b) const X&
c) volatile X&
d) const volatile X&
且沒有其他參數或其他參數都有默認值,那麼這個函數是拷貝構造函數.

類中可以存在超過一個拷貝構造函數。

class X {  
public:        
  X(const X&);      // const 的拷貝構造 
  X(X&);            // 非const的拷貝構造 
}; 

如果一個類中只存在一個參數爲 X& 的拷貝構造函數,那麼就不能使用const X或volatile X的對象實行拷貝初始化





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章