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

                                拷貝構造函數和賦值構造函數的異同
     由於並非所有的對象都會使用拷貝構造函數和賦值函數,程序員可能對這兩個函數有些輕視。請先記住以下的警告,在閱讀正文時就會多心:如果不主動編寫拷貝構造函數和賦值函數,編譯器將以“位拷貝”的方式自動生成缺省的函數。倘若類中含有指針變量,那麼這兩個缺省的函數就隱含了錯誤。(位拷貝可參考:http://www.cnblogs.com/chenyuming507950417/archive/2012/03/10/2389142.html

    以類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 被釋放了兩次。

     拷貝構造函數和賦值函數非常容易混淆,常導致錯寫、錯用。拷貝構造函數是在對象被創建時調用的,而賦值函數只能被已經存在了的對象調用。以 下程序中,第三個語句和第四個語句很相似,你分得清楚哪個調用了拷貝構造函數,哪個調用了賦值函數嗎?

  
String a(“hello”);
String b(“world”);  
String c = a; // 調用了拷貝構造函數,最好寫成 c(a)  
c = b; // 調用了賦值函數

  本例中第三個語句的風格較差,宜改寫成String c(a) 以區別於第四個語句。


  類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 返回的將是垃圾。


  偷懶的辦法處理拷貝構造函數與賦值函數
  如果我們實在不想編寫拷貝構造函數和賦值函數,又不允許別人使用編譯器生成的缺省函數,怎麼辦?
  偷懶的辦法是:只需將拷貝構造函數和賦值函數聲明爲私有函數,不用編寫代碼。

  例如:

  
class A  
{      
  private:      
  A(const A &a); // 私有的拷貝構造函數      
  A & 
  operator =(const A &a); // 私有的賦值函數  
};

  如果有人試圖編寫如下程序:

  
A b(a); // 調用了私有的拷貝構造函數  
b = a; // 調用了私有的賦值函數

  編譯器將指出錯誤,因爲外界不可以操作A 的私有函數。


一、拷貝構造,是一個的對象來初始化一邊內存區域,這邊內存區域就是你的新對象的內存區域賦值運算,對於一個已經被初始化的對象來進行operator=操作

class A;    
A a; 
A b=a;    //拷貝構造函數調用  
//或  
A b(a);    //拷貝構造函數調用  
///////////////////////////////////    
A a; 
A b;  
b =a;    //賦值運算符調用

你只需要記住,在C++語言裏,  

String  s2(s1);  String  s3=s1;

以上只是語法形式的不同,意義是一樣的,都是定義加初始化,都調用拷貝構造函數。
二、一般來說是在數據成員包含指針對象的時候,應付兩種不同的處理需求的 一種是複製指針對象,一種是引用指針對象 copy大多數情況下是複製,=則是引用對象的 。   
例子:  

   class A     
{         
  int nLen;         
  char *pData;     
}

   顯然  

       A a,b;

   a=b的時候,對於pData數據存在兩種需求  
   第一種copy 

       a.pData =new char [nLen];        
memcpy(a.pData,b.pData,nLen);

   另外一種(引用方式): 

a.pData = b.pData

   通過對比就可以看到,他們是不同的  
   往往把第一種用copy使用,第二種用=實現
   你只要記住拷貝構造函數是用於類中指針,對象間的COPY  

三、和拷貝構造函數的實現不一樣    
       拷貝構造函數首先是一個構造函數,它調用的時候產生一個對象,是通過參數傳進來的那個對象來初始化,產生的對象。  
       operator=();是把一個對象賦值給一個原有的對象,所以如果原來的對象中有內存分配要先把內存釋放掉,而且還要檢查一下兩個對象是不是同一個對象,如果是的話就不做任何操作。
        還要注意的是拷貝構造函數是構造函數,不返回值  
       而賦值函數需要返回一個對象自身的引用,以便賦值之後的操作  
        你肯定知道這個:  

    int a, b;       
b = 7;       
Func( a = b );    //把i賦值後傳給函數Func(int)

   同理:  

      
CMyClass  obj1, obj2;
obj1.Initialize();
Func2( obj1  =  obj2  );//如果沒有返回引用,是不能把值傳給Func2的

       注:  

CMyClass  &
CMyClass:: operator  = ( CMyClass &other )         
{              
  if( this== &other )                   
  return  *this;                  //賦值操作...                
  return  *this          
}


        賦值運算符和賦值構造函數都是用已存在的B對象來創建另一個對象A。不同之處在於:賦值運算符處理兩個已有對象,即賦值前B應該是存在的;複製構造函數是生成一個全新的對象,即調用複製構造函數之前A不存在。
  CTemp a(b); //複製構造函數,C++風格的初始化
  CTemp a=b; //仍然是複製構造函數,不過這種風格只是爲了與C兼容,與上面的效果一樣,在這之前a不存在,或者說還未構造好。
  CTemp a;
  a=b; //賦值運算符
  在這之前a已經通過默認構造函數構造完成。
  實例總結:
  重點:包含動態分配成員的類應提供拷貝構造函數,並重載"="賦值操作符。
  以下討論中將用到的例子:

class CExample{      
  public:      
    CExample(){pBuffer=NULL; nSize=0;}      
    ~CExample(){delete pBuffer;}      
    void Init(int n){ pBuffer=new char[n]; nSize=n;}      
  private:      
    char *pBuffer; //類的對象中包含指針,指向動態分配的內存資源      
    int nSize;  
};

  這個類的主要特點是包含指向其他資源的指針。
  pBuffer指向堆中分配的一段內存空間。
  一、拷貝構造函數
  調用拷貝構造函數1

int main(int argc, char* argv[])
{      
  CExample theObjone;     
  theObjone.Init(40);      //現在需要另一個對象,需要將他初始化稱對象一的狀態     
  CExample theObjtwo=theObjone;//拷貝構造函數       // ...  
}

  語句"CExample theObjtwo=theObjone;"用theObjone初始化theObjtwo。
  其完成方式是內存拷貝,複製所有成員的值。
  完成後,theObjtwo.pBuffer==theObjone.pBuffer。
  即它們將指向同樣的地方(地址空間),指針雖然複製了,但所指向的空間內容並沒有複製,而是由兩個對象共用了。這樣不符合要求,對象之間不獨立了,併爲空間的刪除帶來隱患。
  所以需要採用必要的手段來避免此類情況。
  回顧以下此語句的具體過程:通過拷貝構造函數(系統默認的)創建新對象theObjtwo,並沒有調用theObjtwo的構造函數(vs2005試驗過)。
  可以在自定義的拷貝構造函數中添加輸出的語句測試。
  注意:
  對於含有在自由空間分配的成員時,要使用深度複製,不應使用淺複製。
  調用拷貝構造函數2
  當對象直接作爲參數傳給函數時,函數將建立對象的臨時拷貝,這個拷貝過程也將調同拷貝構造函數。
  例如

  
BOOL testfunc(CExample obj);  
testfunc(theObjone); //對象直接作爲參數。  
BOOL testfunc(CExample obj)  
{      
  //針對obj的操作實際上是針對複製後的臨時拷貝進行的  
}

  調用拷貝構造函數3
  當函數中的局部對象被被返回給函數調者時,也將建立此局部對象的一個臨時拷貝,拷貝構造函數也將被調用

CTest func()
{      
  CTest theTest;      
  return theTest  
}

  二、賦值符的重載
  下面的代碼與上例相似

int main(int argc, char* argv[])
{      
  CExample theObjone;      
  theObjone.Init(40);      
  CExample theObjthree;      
  theObjthree.Init(60);    //現在需要一個對象賦值操作,被賦值對象的原內容被清除,並用右邊對象的內容填充。
  theObjthree=theObjone;     
  return 0; 
}

  也用到了"="號,但與"一、"中的例子並不同,"一、"的例子中,"="在對象聲明語句中,表示初始化。更多時候,這種初始化也可用括號表示。
  例如 CExample theObjone(theObjtwo);
  而本例子中,"="表示賦值操作。將對象theObjone的內容複製到對象theObjthree;,這其中涉及到對象theObjthree原有內容的丟棄,新內容的複製。
  但"="的缺省操作只是將成員變量的值相應複製。舊的值被自然丟棄。
   由於對象內包含指針,將造成不良後果:爲了避免內存泄露,指針成員將釋放指針所指向的空間,以便接受新的指針值,這正是由賦值運算符的特徵所決定的。但 如果是"x=x"即自己給自己賦值,會出現什麼情況呢?x將釋放分配給自己的內存,然後,從賦值運算符右邊指向的內存中複製值時,發現值不見了。
  因此,包含動態分配成員的類除提供拷貝構造函數外,還應該考慮重載"="賦值操作符號。
  類定義變爲:

class CExample
{
  //...
  CExample(const CExample&); //拷貝構造函數
  CExample& operator = (const CExample&); //賦值符重載
  //...
};
//賦值操作符重載
CExample & 
CExample::operator = (const CExample& RightSides)
{
  nSize=RightSides.nSize; //複製常規成員
  char *temp=new char[nSize]; //複製指針指向的內容 
  memcpy(temp, RightSides.pBuffer, nSize*sizeof(char)); 
  delete []pBuffer; //刪除原指針指向內容 (將刪除操作放在後面,避免X=X特殊情況下,內容的丟失)
  pBuffer=temp; //建立新指向 
  return *this;
}

  三、拷貝構造函數使用賦值運算符重載的代碼。

CExample::CExample(const CExample& RightSides)
{
  pBuffer=NULL;
  *this=RightSides //調用重載後的"="
}

轉載地址:http://blog.chinaunix.net/uid-25808509-id-354211.html


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