複製建構函式(拷貝建構函式)

來源:互聯網
上載者:User
也許很多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,預設的複製建構函式被調用。
  最好的方法是建立自己的複製建構函式而不要指望編譯器建立,這樣就能保證程式在我們自己的控制之下。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.