[轉載]http://www.csdn.net/develop/article/22/22033.shtm

構造函數(中)

?

三、復制構造函數

?

??? 1.存在的理由

?

??? 廚 師做烹飪的時候總要往鍋里加入各式各樣的調料,調料的種類、數量在相當大的程度上就決定了菜肴的口感;經驗豐富的廚師總是擅長于根據顧客的品味差異來調節 調料的投入,以迎合顧客的喜好。我們在炮制對象的時候亦如此:通過重載不同具有參數表的構造函數,可以按我們的需要對新創建的對象進行初始化。譬如,對于 復數類Complex,我們可以在創建時指定實部、虛部,它通過“投入”的兩個double參數來實現;而對于整型數組類IntArray,我們亦可以在 構造函數中“投入”一個int值作為數組對象的初始大小,如果需要,我們還可以再“撒進”一個內部數組作為數組各元素的初始值,等等。嗯,總之,就是說我 們可以通過向類的構造函數中傳入各種參數,從而在對象創建之初便根據需要可對它進行初步的“調制”。假使我們已經做好了一道菜,呃,不,是已經有了一個對 象,比如說,一個復數類Complex的對象c,現在我們想新創建一個復數對象d,使之與c完全相等,應該怎么辦?噢,有了上節的經驗,我知道你會興沖沖 地走到電腦,敲出類似于下面的代碼:

?

??? Complex d(c.getRe(), c.getIm());???? // getRe()與getIm()分別返回復數的實、虛部

?

??? 很 好,它可以正確的工作,不是嗎?不過,再回過頭兩欣賞幾眼之后,你是否和我一樣開始覺得似乎這樣有些冗長?而且,應該看到復數類是一個比較簡單的抽象數據 類型,它提供的公有接口可以讓我們訪問到它所有的私有成員變量,所以我們才得以獲得c的所有的“隱私”數據來對d進行初始化;但有相當多的類,是不能也不 該訪問到它所有的私有對象的,退一步說,就算可以完全訪問,我們的構造函數參數表也未必總能精細地、一絲不茍地刻畫對象,例如對于IntArray類的某 個對象,例如a,可能它會包含一千個元素,但我們不可能寫

?

??? IntArray copy_of_a(a.size(), a[0], a[1], a[2], ..., a[999]);? // 足夠詳細的刻畫,同時也足夠愚蠢

?

??? 唔, 看來我們錯就錯在試圖用數量有限的輔助參數來刻畫我們的對象,而它往往是不合適的。要想以對象a來100%地確定對象b,就必須讓對象b掌握對象a的 100%的信息,也就是說,構造函數中所傳入的參數應該包含對象a的100%的信息。誰包含了“對象a的100%的信息”呢?看上去似乎是一道難題,哦, 我們似乎被前面各個雜亂的參數表弄得頭暈腦脹,但我們不應該忘卻從簡單的方面考慮問題的法則,包含“對象a的100%的信息”的家伙,不應該是一群數量巨 大的變量,不應該是一組令人恐懼的參數,它應該就是...就是對象a本身。

??? 啊哈,真是廢話。別管這么多,我們現在要讓復數d初始化之后立刻等于復數c,就可以寫

?

??? Complex d(c);??? // 嗯...完美的表示

?

??? “等等...”你也許會提醒說,“你好像還沒有定義與之對應的構造函數。”

??? 這 是一個非常好心的提醒,不過我可以帶著愉悅的心情告訴你:C++對于“以與自己同類型的對象為作為唯一參數傳入的構造函數”已經做了默認的定義,對于這個 默認的構造函數所創建的對象,將與作為參數傳入的對象具有完全相同的內容:實際上,這個過程一般是以“位拷貝”的方式進行的,即是將參數對象所占的那一塊 內存的內容“一股腦”地拷貝到新創建的對象所處的內存區塊中,這樣做的結果,便使得新創建的對象與參數對象內有完全相同的成員變量值,無論是公有的還是私 有的,因而,我們也就得以以一個已存在的對象來初始化一個新的對象,當然,兩者應該是同一類型的。

??? 你也許會對“為何C++要默認提供這樣一個構造函數”感興趣。實質上,不僅僅是上面的“聲明對象并初始化”的例子要使用到這個特性,在一些更普遍的情況、同時也是我們所不注意的情況中,必須要使用到這個功能,例如:進行值傳遞的函數調用。考慮下面的代碼片斷:

?

??? void swap(int a, int b)

??? {

??????? int temp = a;

??????? a = b;

??????? b = temp;

??? }

?

??? int main()

??? {

????? ?int x = 1, y = 2;

?????? swap(x, y);??? // 噢...

?????? ... // others

?????? return 0;

??? }

?

??? 問 題:執行了swap(x, y)之后,x, y的值分別是多少?毫無疑問,當然是x==1, y==2,因為執行swap時,交換的并不是x,y變量的值,而只是它們的一個副本。我們可以從某個角度認為:在調用swap函數時,將會“新創建”兩個 變量a, b,而a, b分別以x, y的值進行初始化,像這樣:

?

??? int a(x), b(y);

??? ...? // 此后的操作都與x, y無關了

?

??? 同樣,假如有這樣一個函數:

??? void swap(Complex a, Complex b)

??? {

??????? ...

??? }

??? 在出現諸如swap(p, q)這樣的調用時,也會對p, q進行復制操作,類似于:Complex a(p), b(q);

??? 與參數的傳入一樣,函數在返回時(如果有返回的話),只要不是返回某個引用,則也會出現類似的復制操作(這里不考慮某些省卻復制步驟的編譯優化)。

??? 所以,假如系統不會為我們默認定義這樣的特殊的構造函數,我們將不能定義任何出現Complex類型的參數,或者返回值為Complex類型的函數,而這簡直是沒有道理的,至少,看上去不是那么好令人接受。

??? 由于這樣的構造函數可以看作是從一個既有的同型對象中“復制”創建出一個新對象,所以也把這個構造函數稱為復制構造函數,或拷貝構造函數(copy constructor).實質上它與其它的構造函數沒有區別,只是:

??? 1.如果沒有明確給出,系統會默認提供一個復制構造函數(類似于不帶參數的構造函數,以及析構函數);

??? 2.進行值傳遞的函數調用或返回時,系統將使用此構造函數(相對于其它的通常只存在人為調用的構造函數)。

?

??? 噫,以上便是有關復制構造函數的講解。時間不早,該休息了。朋友,祝您晚安,下次見...

??? 哦, 不不不,弄錯了,沒有這么簡單。我們...我們才剛剛開始...唔,至少我還得在睡前把這篇文章打完,而且下面似乎內容不少,總之,還有相當多的因素要考 慮。嗯,你可能說,C++提供的默認復制構造函數不是已經盡職盡責地、一個bit不漏地為我們將對象原原本本地克隆過去么,那么所得的新對象肯定與被復制 對象完全一樣,還有什么要考慮的?

??? 問 題就出在對于“完全一樣”的理解上。我們說兩個三角形完全一樣,是指它們具有對應相同的邊、角;我們說兩個復數完全一樣,是指它們具有對應相等的實部、虛 部;我們說兩個文件完全一樣,是指它們的內容一絲不茍地相同...那我們說某個類型的兩個對象完全一樣,當然,從樸素的意義上說,應該是指這兩個對象具有 相同的成員,包括成員函數與成員變量;不過,同型對象當然有相同的成員函數,所以更確切地說,完全相同即是具有相同的成員變量。

??? 但C ++中的類的對象并非、也不應該僅被視為綣縮于內存中某個角落的一系列二進制數據,那是機器的觀點。事實上,對象作為現實模型的一個描述,它本身應該具有 一定的抽象而來意義。一個Orange類的對象,在編程時也許我們更多的時候會有意無意地將其作為一個活生生的桔子進行考慮,而非一撮二進制流。C++的 面向對象抽象使我們更好地刻畫現實模型,并進行正確的、恰當的使用。因此,當我們說兩個對象“完全相等”,基于抽象的思想,我們所指的,或者說所要求的, 只是對象所代表的現實模型的意義上的相等,至于在二進制層面的實現上,它們內部的0101是否一樣,倒不是我們最關心的,當然,可能在許多情況下,兩者是 等同的,但也不盡然。譬如兩個IntArray數組對象,當然,它們各包含了一個整型指針作為私有成員,以存放數組所處內存空間的首地址。但我們說這兩個 數組對象相同,更多意義上指的是它們含有相同的維數、尺寸,以及對應相等的元素,至于存放首地址的指針值,盡管它們在大多數情況下都不相等,但這并不妨礙 我們認為兩個數組相同。在這個例子中,“對象”所代表的概念上的意義與它在機器層面的意義出現了分岐,程序語言的任務之一就在于提供更恰當的抽象,使程序 具有某種吻合于現實模型的條理性。所以兩者出現分岐之時,既然我們使用的是具有相當抽象能力的高級語言,就應當傾向于尊重現實的概念,而不是返樸歸真。

??? 說 了這么多,現在該回過頭來看一看默認復制構造函數所采用的“完全照搬”復制模式將有可能帶來怎樣的“概念層面”與“機器層面”的矛盾。剛才我們已經抓到了 IntArray類的尾巴,現在來看看,假如使用代表著“機器層面理解”的默認復制函數對它進行復制構造,會發生什么事情。嗯,沒錯,新創建的 IntArray對象--不妨稱為b--將具有與原對象--不妨稱為a--完全相同的成員變量,當然也包括用于存儲數組內存空間首地址的指針成員,也就是 說,a與b實際上是“共享”了同一塊內存以存放“它們”的數據。從此,如果我改變a數組中的某個元素的值,則b中相應元素也會改變,反之亦然--我們的程 序鬧鬼了。更為嚴重的是,如果我們在IntArray的析構函數中加入了釋放數組內存的代碼(幾乎所有的容器都會這樣做),那么由于有多個對象共享同一塊 內存,這塊可憐的內存將會被釋放多次,事實上當第二次來臨時我們的程序很可能就已經崩潰了,這種情況在值傳遞調用函數時最為典型。

??? 我 們概念意義上對“數組復制”的理解是產生一個新數組,使之與原數組的元素均對應相同,而絕不是什么共享內存的鬼把戲。但我們忠厚的C++編譯器可不了解這 些,畢竟它只是機器,所以也只會盡職盡責地為我們施行位拷貝。這樣一來,我們就有義務把我們對概念本身的理解告訴C++,當然,這個途徑就是顯示地定義一 個復制構造函數。

?

??? 2.關于聲明的討論

?

??? 首 先,以IntArray類為例,我們來看看復制構造函數的聲明應該是什么樣子:構造函數是沒有返回值的,復制構造函數當然也不例外,因此我們只須考慮參 數。復制構造函數只有一個參數,由于在創建時傳入的是同種類型的對象,所以一個很自然的想法是將該類型的對象作為參數,像這樣:

?

??? IntArray(IntArray a);

?

??? 不 幸的是,即使是這樣樸實無華的聲明也隱含了一個微妙的錯誤,呵,我們來看看:當某個時候需要以一個IntArray對象的值來為一個新對象進行初始化時, 當然,編譯器會在各個重載的構造函數版本(如果有多個的話)搜尋,它找到的這個版本,發現聲明參數與傳入的對象一致,因此該構造函數將會被調用。目前為 止,一切都在我們的意料之中,但問題很快來了:該函數的參數我們使用了值傳遞的方式,按照前面的分析,這需要調用復制構造函數,于是編譯器又再度搜尋,最 后當然又找到了它,于是進行調用,但同樣地,傳參時又要進行復制,于是再調用...這個過程周而復始,每次都是到了函數入口處就進行遞歸,直到堆棧空間耗 盡,程序崩潰...

??? 當 然,這樣的好戲在現實中不大會出現,因為編譯器會及時發現這一明顯是問題的問題,并報告錯誤,終止編譯;所以接下來我們還得想想其它辦法。我們剛才之所以 沒有獲得想當然的成功是由于在傳參時出現了復制構造函數的調用--而這本來就是它需要解決的操作--從而產生無窮遞歸。由是觀之,值傳遞看來是行不通的 了;我想C語言的用戶這時很快會反應到與值傳遞對應的方式:地址傳遞(傳址),于是聲明變為這樣:

?

??? IntArray(IntArray *p);

?

??? 只作為一般的構造函數,它應該可以運行得很好,但別忘了我們要提供的是復制構造函數,它要求能夠接受一個同類型對象,像這樣:

?

??? IntArray a;

??? ... // 對a操作

??? IntArray b(a);

?

??? 而不是接受指針:

?

??? IntArray a;

??? ... // 對a操作

??? IntArray b(&a);? // 還要取地址?當然,它可以正確運行,但...

?

??? 否則,雖然在初始化對象時可以像上面一樣人為加一個取址符,但在函數參數表中(或者函數返回)進行值傳遞時,編譯器可不知道在找不著合適定義的情況下牽就選擇你的指針版本。

??? 既然復制構造函數括號里放的必須是某個對象,但又不能通過值傳遞,也不能通過傳址代替,解決的方案,我想你一定早想到了。沒錯,就是使用引用:

?

??? IntArray(IntArray& a);

?

??? 由于引用產生的只是一個別名,而實質上使用的還是“原來的”被引用的對象,所以,當然,嗯,也就不會存在什么參數復制的問題,從而不會再去調用復制構造函數--也就是它自己!

??? 成功之后我們再來看看能不能再做一些改進:我們在復制對象的過程中,當然,從語義上說,一般不會改變被復制的“母體對象”的值。我們難以想象,諸如:

?

??? Complex b(a);

?

的語句過后,會使復數a 變得面目全非;同時,假如“母體對象”是const類型,將意味著我們不能用它來初始化其它對象:因為參數IntArray& a不能保證a不會被修改!將const常量傳給非const引用會導致編譯出錯,但這個限制顯然并非我們所要的;并且,我們有可能不小心的情況在復制構造 函數中修改了本不希望改動的“母體對象”,而這一切將很難在今后查出。綜上所述,我們通常會給復制構造函數的引用參數前加上const進行修飾:

?

??? IntArray(const IntArray& a);

?

??? 這樣一來就更完美了,它避免了上述的問題;而且即使是非const的對象也可以作為const型的引用(如前文所分析,反過來就不可以)。

?

??? 3.定義的實現

?

??? 好, 我們已經找到了復制構造函數的一個合適的參數,對于聲明的討論也就告一段落了。現在我們還是以IntArray為例,來看看它的實現應當是什么樣子。如果 你讀過《構造函數(上)》,那么還記得上一節開頭我所給出的有關IntArray的聲明么?如果你的回答是Yes,那么我很佩服你的用心,不過我自己是記 不得了,為了我自己,以及那些同我一樣的朋友和暫時沒有讀過上節內容的朋友,我把上一節的IntArray主要部分聲明粘貼如下(其實很簡單):

?

class IntArray

?

{

?

public:

?

??? ... // others

?

private:

?

??? int *_p;

?

??? int _size;

?

};

?

??? 如 你所推測,IntArray使用整型指針_p存儲在自由存儲區上以new表達式分配得來的內存空間的首地址,而_size則存放該空間可以容納的整型元素 的個數,換句話說,它也就代表了數組的尺寸。以機器的觀點,在IntArray中,_p和_size是標識兩個IntArray對象是否相同的特征變量 (我們假設pulic區段所聲明的都是成員函數,而沒有成員變量),但從我們對數組概念上的理解,_size和_p所指的內存中元素是否均對應相同,才是 數組相同的意義;換言之,_size和_p所指的內存的內容(而不是_p存儲的地址值)才是構成我們“概念理解上”的數組的信息。因而在進行復制的時候, 基于尊重“概念抽象”的觀點,應當對_p所指的內存空間進行復制,而不是簡單地復制_p本身。這就是我們在自定義的復制構造函數中所要表明的;下面是一個 可能的定義,我把分析放在注釋部分:

?

??? IntArray(const IntArray& a)

??? {

??????? _size = a._size;? // 我們顯示定義了復制構造函數之后,默認構造函數就不存在了,

????????????????????????? // 所以必須自己實現所有必要的成員變量的復制

?

??????? if (_size > 0)??? // 只有a的尺寸大于0,也就是a有內容時,才需要復制

??????? {

???????????? _p = new int[_size];? // 構造函數總是在創建新對象時調用,所以別忘了分配內存

???????????? for (int i = 0; i < _size; ++i)? // 逐個復制a中的內容

???????????????? _p[i] = a._p[i];

???????? }

???????? else

???????????? _p = 0;???? // 安全起見,我們把零內容數組的指針設為0

??? }

?

??? 嗯, 再補充一點。如果你的程序非常在意效率,那么你可以用諸如memcopy()的內存拷貝函數把整塊a._p所指的內存復制過來,一般這樣會比使用循環快一 些。不過在C++中對這個函數的使用不是不需要經過慎重考慮的:和相當部分的默認復制構造函數一樣,它只是簡單機械地拷貝內存,所以你如果將此函數應用在 諸如IntArray的對象上,以希望“有效率地”拷貝多個IntArray對象,那么幾乎一定會出現問題:它不會知道應該拷貝_p所指的內容,因而將產 生前面所說的種種問題;甚至我們顯式定義了IntArray的復制構造函數,它也不會調用--因為你使用它就意味著你要求的是“位拷貝”,而不是“復制構 造”。所以,在C++中,只有當memcopy應用在內置基本類型上時,我才敢保證它是安全的。當然,如果你把握不準,那么在C++中把這個C時代遺留下 來的函數忘了不失為一個很好的對策。

?

??? 4.組合或繼承情況下的復制構造

?

??? 不知你注意到沒有,我剛剛說memcopy()的機械拷貝只是和“相當部分”的默認復制構造函數一樣,呃,應當反過來說,只是“相當部分”的默認復制構造函數采用“位拷貝”模式。咦,難道還有某種神秘的力量在控制著另一種默認復制構造函數?

??? 先考慮以下例子:假如我現在要設計一個類,不妨叫A,它包含了一個IntArray對象作為其中一個成員,當然,這是不奇怪的,像這樣:

?

class A

{

public:

??? ... // others

private:

??? IntArray _array; // that's it

??? ... // others

};

?

??? 然后我的代碼中出現了A的對象的復制構造,這也是很有可能的,像這樣一個函數:

?

A dosomething(A a)

{

??? ...????? // 一些操作

??? A b(a);? // 以a復制構造出b

??? ...

??? return b;

}

?

??? 以 上的片斷就隱含了三個關于A對象的復制構造(你看得出來嗎?),但如果我沒有為A定義復制構造函數,如前面所說,編譯器當然就會建立一個默認復制構造函 數,然后在需要的時候調用它。問題在于,這個默認復制構造函數假如還是傻呵呵地采用簡單的位拷貝,那至少可以斷定我們的_array成員將遭遇不測。當 然,你或許會責怪我沒有為A添置一個復制構造函數,但我可以說,而且是很有理由地辯解說,class A的其余成員并不需要提供復制構造函數,至于IntArray類型成員,我只是這個類的用戶,它封裝好的內部機制不應當由我負責,事實上我也無從負責:即 使我真的愿意專門為之設計一個復制構造函數,我又該怎么實現呢?作為用戶,我得到的只是IntArray的接口,也就是說我只有權利使用它,而無法關心也 不該它內部是怎樣運作的,而復制構造函數通常需要涉及上述內容。

??? 作 為一門成功的語言,C++當然不能允許存在上述的尷尬。事實上,假如一個類存在需要調用(用戶定義的,可能是間接的)復制構造函數的成員對象,而類本身又 沒有提供復制構造函數,則其默認構造函數不是簡單地采用位拷貝模式,而是將其對象“逐一”分別拷貝,對于需要調用復制構造函數的對象,則進行相應的調用, 以保持復制構造的概念上的意義。

??? 對于繼承的情況也是類似,假如有一個SuperIntArray類繼承至IntArray:

?

class SuperIntArray: public IntArray

{

public:

??? ... // something

private:

??? ... // something

};

?

??? 即使SuperIntArray本身沒有提供復制構造函數,在其對象需要復制構造時,對基類IntArray部分的構造,同樣也會調用IntArray(const IntArray&)來實現。

??? 嗯,如果前面所提到的組成與繼承的情況中,新的類型顯示提供了復制構造函數,則相應成員,或者基類的復制構造函數也同樣會被調用。實際上前一節我們提到構造函數時已經就這個問題討論過了,所以這一部分基本上沒有太多的新意:別忘了,復制構造函數只不過是構造函數的一種。

?