轉自此處,貌似博主也是轉別人的
也許很多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 |
|
1 class String 2 { 3 public: 4 String(); //構造函數 5 String(const String &s); //復制構造函數 6 ~String(); //析構函數 7 8 // 接口函數 9 void set(char const *data); 10 char const *get(void); 11 12 private: 13 char *str; //數據成員ptr指向分配的字符串 14 }; 15 String ::String(const String &s) 16 { 17 str = new char[strlen(s.str) + 1]; 18 strcpy(str, s.str); 19 }
|
 |
我們也常用無名對象初始化另一個對象,例如:
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 |
|
1 #include <iostream.h> 2 #include <string.h> 3 class withCC 4 { 5 public: 6 withCC(){} 7 withCC(const withCC&) 8 { 9 cout<<"withCC(withCC&)"<<endl; 10 } 11 }; 12 13 class woCC 14 { 15 enum{bsz = 100}; 16 char buf[bsz]; 17 public: 18 woCC(const char* msg = 0) 19 { 20 memset(buf, 0, bsz); 21 if(msg) strncpy(buf, msg, bsz); 22 } 23 void print(const char* msg = 0)const 24 { 25 if(msg) cout<<msg<<":"; 26 cout<<buf<<endl; 27 } 28 }; 29 30 class composite 31 { 32 withCC WITHCC; 33 woCC WOCC; 34 public: 35 composite() : WOCC("composite()"){} 36 void print(const char* msg = 0) 37 { 38 WOCC.print(msg); 39 } 40 }; 41 42 void main() 43 { 44 composite c; 45 c.print("contents of c"); 46 cout<<"calling composite copy-constructor"<<endl; 47 composite c2 = c; 48 c2.print("contents of c2"); 49 }
|
 |
類withCC有一個復制構造函數,類woCC和類composite都沒有顯式定義復制構造函數。如果在類中沒有顯式定義復制構造函數,則編譯器將自動地創建一個缺省的構造函數。不過在這種情況下,這個構造函數什么也不作。
類composite既含有withCC類的成員對象又含有woCC類的成員對象,它使用無參的構造函數創建withCC類的對象WITHCC(注意內嵌的對象WOCC的初始化方法)。
在main()函數中,語句:
composite c2 = c;
通過對象C初始化對象c2,缺省的復制構造函數被調用。
最好的方法是創建自己的復制構造函數而不要指望編譯器創建,這樣就能保證程序在我們自己的控制之下。