拷貝構造函數

http://blog.csdn.net/feiyond/article/details/1807068#

複製構造函數(拷貝構造函數)

分類: 技術文章7383人閱讀評論(18)收藏舉報
也許很多C++的初學者都知道什麼是構造函數,但是對複製構造函數(copy constructor)卻還很陌生。對於我來說,在寫代碼的時候能用得上覆制構造函數的機會並不多,不過這並不說明覆制構造函數沒什麼用,其實複製構造函數能解決一些我們常常會忽略的問題。
爲了說明覆制構造函數作用,我先說說我們在編程時會遇到的一些問題。對於C++中的函數,我們應該很熟悉了,因爲平常經常使用;對於類的對象,我們也很熟悉,因爲我們也經常寫各種各樣的類,使用各種各樣的對象;對於指針的操作,我們也不陌生吧?嗯,如果你還不瞭解上面三個概念的話,我想這篇文章不太適合你,不過看看也無礙^_^。我們經常使用函數,傳遞過各種各樣的參數給函數,不過把對象(注意是對象,而不是對象的指針或對象的引用)當作參數傳給函數的情況我們應該比較少遇見吧,而且這個對象的構造函數還涉及到一些內存分配的操作。嗯,這樣會有什麼問題呢?
把參數傳遞給函數有三種方法,一種是值傳遞,一種是傳地址,還有一種是傳引用。前者與後兩者不同的地方在於:當使用值傳遞的時候,會在函數裏面生成傳遞參數的一個副本,這個副本的內容是按位從原始參數那裏複製過來的,兩者的內容是相同的。當原始參數是一個類的對象時,它也會產生一個對象的副本,不過在這裏要注意。一般對象產生時都會觸發構造函數的執行,但是在產生對象的副本時卻不會這樣,這時執行的是對象的複製構造函數。爲什麼會這樣?嗯,一般的構造函數都是會完成一些成員屬性初始化的工作,在對象傳遞給某一函數之前,對象的一些屬性可能已經被改變了,如果在產生對象副本的時候再執行對象的構造函數,那麼這個對象的屬性又再恢復到原始狀態,這並不是我們想要的。所以在產生對象副本的時候,構造函數不會被執行,被執行的是一個默認的構造函數。當函數執行完畢要返回的時候,對象副本會執行析構函數,如果你的析構函數是空的話,就不會發生什麼問題,但一般的析構函數都是要完成一些清理工作,如釋放指針所指向的內存空間。這時候問題就可能要出現了。假如你在構造函數裏面爲一個指針變量分配了內存,在析構函數裏面釋放分配給這個指針所指向的內存空間,那麼在把對象傳遞給函數至函數結束返回這一過程會發生什麼事情呢?首先有一個對象的副本產生了,這個副本也有一個指針,它和原始對象的指針是指向同塊內存空間的。函數返回時,對象的析構函數被執行了,即釋放了對象副本里面指針所指向的內存空間,但是這個內存空間對原始對象還是有用的啊,就程序本身而言,這是一個嚴重的錯誤。然而錯誤還沒結束,當原始對象也被銷燬的時候,析構函數再次執行,對同一塊系統動態分配的內存空間釋放兩次是一個未知的操作,將會產生嚴重的錯誤。
上面說的就是我們會遇到的問題。解決問題的方法是什麼呢?首先我們想到的是不要以傳值的方式來傳遞參數,我們可以用傳地址或傳引用。沒錯,這樣的確可以避免上面的情況,而且在允許的情況下,傳地址或傳引用是最好的方法,但這並不適合所有的情況,有時我們不希望在函數裏面的一些操作會影響到函數外部的變量。那要怎麼辦呢?可以利用複製構造函數來解決這一問題。複製構造函數就是在產生對象副本的時候執行的,我們可以定義自己的複製構造函數。在複製構造函數裏面我們申請一個新的內存空間來保存構造函數裏面的那個指針所指向的內容。這樣在執行對象副本的析構函數時,釋放的就是複製構造函數裏面所申請的那個內存空間。
除了將對象傳遞給函數時會存在以上問題,還有一種情況也會存在以上問題,就是當函數返回對象時,會產生一個臨時對象,這個臨時對象和對象的副本性質差不多。
 
拷貝構造函數,經常被稱作X(X&),是一種特殊的構造函數,他由編譯器調用來完成一些基於同一類的其他對象的構件及初始化。它的唯一的一個參數(對象的引用)是不可變的(因爲是const型的)。這個函數經常用在函數調用期間於用戶定義類型的值傳遞及返回。拷貝構造函數要調用基類的拷貝構造函數和成員函數。如果可以的話,它將用常量方式調用,另外,也可以用非常量方式調用。
在C++中,下面三種對象需要拷貝的情況。因此,拷貝構造函數將會被調用。
1). 一個對象以值傳遞的方式傳入函數體
2). 一個對象以值傳遞的方式從函數返回
3). 一個對象需要通過另外一個對象進行初始化
以上的情況需要拷貝構造函數的調用。如果在前兩種情況不使用拷貝構造函數的時候,就會導致一個指針指向已經被刪除的內存空間。對於第三種情況來說,初始化和賦值的不同含義是構造函數調用的原因。事實上,拷貝構造函數是由普通構造函數和賦值操作賦共同實現的。描述拷貝構造函數和賦值運算符的異同的參考資料有很多。
拷貝構造函數不可以改變它所引用的對象,其原因如下:當一個對象以傳遞值的方式傳一個函數的時候,拷貝構造函數自動的被調用來生成函數中的對象。如果一個對象是被傳入自己的拷貝構造函數,它的拷貝構造函數將會被調用來拷貝這個對象這樣複製纔可以傳入它自己的拷貝構造函數,這會導致無限循環。
除了當對象傳入函數的時候被隱式調用以外,拷貝構造函數在對象被函數返回的時候也同樣的被調用。換句話說,你從函數返回得到的只是對象的一份拷貝。但是同樣的,拷貝構造函數被正確的調用了,你不必擔心。
如果在類中沒有顯式的聲明一個拷貝構造函數,那麼,編譯器會私下裏爲你制定一個函數來進行對象之間的位拷貝(bitwise copy)。這個隱含的拷貝構造函數簡單的關聯了所有的類成員。許多作者都會提及這個默認的拷貝構造函數。注意到這個隱式的拷貝構造函數和顯式聲明的拷貝構造函數的不同在於對於成員的關聯方式。顯式聲明的拷貝構造函數關聯的只是被實例化的類成員的缺省構造函數除非另外一個構造函數在類初始化或者在構造列表的時候被調用。
拷貝構造函數是程序更加有效率,因爲它不用再構造一個對象的時候改變構造函數的參數列表。設計拷貝構造函數是一個良好的風格,即使是編譯系統提供的幫助你申請內存默認拷貝構造函數。事實上,默認拷貝構造函數可以應付許多情況。
 
附另外一篇關於複製構造函數的文章:

對一個簡單變量的初始化方法是用一個常量或變量初始化另一個變量,例如:
  int m = 80;
  int n = m;
  我們已經會用構造函數初始化對象,那麼我們能不能象簡單變量的初始化一樣,直接用一個對象來初始化另一個對象呢?答案是肯定的。我們以前面定義的Point類爲例:
  Point pt1(15, 25);
  Point pt2 = pt1;
後一個語句也可以寫成:
  Point pt2( pt1);
它是用pt1初始化pt2,此時,pt2各個成員的值與pt1各個成員的值相同,也就是說,pt1各個成員的值被複制到pt2相應的成員當中。在這個初始化過程當中,實際上調用了一個複製構造函數。當我們沒有顯式定義一個複製構造函數時,編譯器會隱式定義一個缺省的複製構造函數,它是一個內聯的、公有的成員,它具有下面的原型形式:
  Point:: Point (const Point &);
可見,複製構造函數與構造函數的不同之處在於形參,前者的形參是Point對象的引用,其功能是將一個對象的每一個成員複製到另一個對象對應的成員當中。
  雖然沒有必要,我們也可以爲Point類顯式定義一個複製構造函數:
  Point:: Point (const Point &pt)
  {
   xVal=pt. xVal;
   yVal=pt. yVal;
  }
  如果一個類中有指針成員,使用缺省的複製構造函數初始化對象就會出現問題。爲了說明存在的問題,我們假定對象A與對象B是相同的類,有一個指針成員,指向對象C。當用對象B初始化對象A時,缺省的複製構造函數將B中每一個成員的值複製到A的對應的成員當中,但並沒有複製對象C。也就是說,對象A和對象B中的指針成員均指向對象C,實際上,我們希望對象C也被複制,得到C的對象副本D。否則,當對象A和B銷燬時,會對對象C的內存區重複釋放,而導致錯誤。爲了使對象C也被複制,就必須顯式定義複製構造函數。下面我們以string類爲例說明,如何定義這個複製構造函數。

 

例題 例10-11
  class String
{
 public:
  String(); //構造函數
  String(const String &s); //複製構造函數
  ~String(); //析構函數

  // 接口函數
  void set(char const *data);
  char const *get(void);

 private:
  char *str; //數據成員ptr指向分配的字符串
};

String ::String(const String &s)
{
 str = new char[strlen(s.str) + 1];
 strcpy(str, s.str);
}

 

我們也常用無名對象初始化另一個對象,例如:
  Point pt = Point(10, 20);
  類名直接調用構造函數就生成了一個無名對象,上式用左邊的無名對象初始化右邊的pt對象。
  構造函數被調用通常發生在以下三種情況,第一種情況就是我們上面看到的:用一個對象初始化另一個對象時;第二種情況是當對象作函數參數,實參傳給形參時;第三種情況是程序運行過程中創建其它臨時對象時。下面我們再舉一個例子,就第二種情況和第三種情況進行說明:
  Point foo(Point pt)
  {
   …
   return pt;
  }
  void main()
  {
   Point pt1 = Point(10, 20);
   Point pt2;
   …
   pt2=foo(pt);
   …
  }
  在main函數中調用foo函數時,實參pt傳給形參pt,將實參pt複製給形參pt,要調用複製構造函數,當函數foo返回時,要創建一個pt的臨時對象,此時也要調用複製構造函數。

缺省的複製構造函數
  在類的定義中,如果沒有顯式定義複製構造函數,C++編譯器會自動地定義一個缺省的複製構造函數。下面是使用複製構造函數的一個例子:
 

例題 例10-12
  #include <iostream.h>
#include <string.h>
class withCC
{
 public:
 withCC(){}
 withCC(const withCC&)
 {
  cout<<"withCC(withCC&)"<<endl;
 }
};

class woCC
{
 enum{bsz = 100};
 char buf[bsz];
public:
 woCC(const char* msg = 0)
 {
  memset(buf, 0, bsz);
  if(msg) strncpy(buf, msg, bsz);
 }
 void print(const char* msg = 0)const
 {
  if(msg) cout<<msg<<":";
  cout<<buf<<endl;
 }
};

class composite
{
 withCC WITHCC;
 woCC WOCC;
public:
 composite() : WOCC("composite()"){}
 void print(const char* msg = 0)
 {
  WOCC.print(msg);
 }
};

void main()
{
 composite c;
 c.print("contents of c");
 cout<<"calling composite copy-constructor"<<endl;
 composite c2 = c;
 c2.print("contents of c2");
}

  類withCC有一個複製構造函數,類woCC和類composite都沒有顯式定義複製構造函數。如果在類中沒有顯式定義複製構造函數,則編譯器將自動地創建一個缺省的構造函數。不過在這種情況下,這個構造函數什麼也不作。
  類composite既含有withCC類的成員對象又含有woCC類的成員對象,它使用無參的構造函數創建withCC類的對象WITHCC(注意內嵌的對象WOCC的初始化方法)。
  在main()函數中,語句:
  composite c2 = c;
通過對象C初始化對象c2,缺省的複製構造函數被調用。
  最好的方法是創建自己的複製構造函數而不要指望編譯器創建,這樣就能保證程序在我們自己的控制之下。

 

 
 

 

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