天天看點

《C++面向對象高效程式設計(第2版)》——4.5 對象複制的語義

本節書摘來自異步社群出版社《c++面向對象高效程式設計(第2版)》一書中的第4章,第4.5節,作者: 【美】kayshav dattatri,更多章節内容可以通路雲栖社群“異步社群”公衆号檢視。

c++面向對象高效程式設計(第2版)

複制對象是oop中的一個很普通的操作。既然在我們的世界中,一切皆是對象,我們肯定會遇到需要某個對象的多個副本的情況。

如第3章所述,在許多不同的情況中都需要複制對象。例如,當按值傳遞(和按值傳回)參數給函數時,就需要制作對象的副本。當函數被調用時,複制操作由語言(編譯器)發起,這是一個隐式進行的操作。

當然,對象的副本也可由程式員通過聲明顯式建立。我們可以編寫如下代碼:

void foo(tperson theperson)

   // foo函數按值接受一個tperson參數

{

   // 此處代碼不重要,已略去。

}

main()

   tperson bar(“foo bar”, “unknown”, 414235056, “6-6-99”);

   // ...

   foo(bar); //調用以對象“bar”為參數的foo函數

}<code>`</code>

調用foo()時,将制作對象bar的副本。在c++中,用複制構造函數完成這樣的複制操作,即通過現有對象制作一個該對象的副本。我們在tperson類中尚未實作複制構造函數,是以,編譯器會生成預設複制構造函數(default copy constructor):

`

tperson::tperson(const tperson&amp; source)`

該複制構造函數将執行資料成員的逐個成員複制(memberwise copy)。複制構造函數中的代碼類似圖4-8。

class tpoint2d {

  public:

    tpoint2d(double x = 0.0, double y = 0.0);

    distanceto(const tpoint2d&amp; otherpoint);

    // 複制構造函數和指派操作符省略 – 由編譯器提供

    // ... 其他細節省略

  private:

    double _xcoordinate;

    double _ycoordinate;

};<code>`</code>

編譯器生成的簡單複制構造函數可以完全滿足需要。該類的對象包含兩個雙精度數,隻需複制它們的值即可。

回到tperson類,我們并不希望複制_name和_address資料成員的值,因為它們是指針。我們希望為它們所指向的内容配置設定足夠的記憶體,然後複制那些内容。在複制操作完成後,源對象和目的對象之間不會共享任何東西。這就是深複制。

smalltalk:

smalltalk為所有類都提供了兩種複制對象的方法shallowcopy和deepcopy。在smalltalk系統中,所有對象都可以使用這些方法。shallowcopy方法建立一個新對象,但該對象與原始對象共享狀态;deepcopy方法複制對象及其狀态,而且這種複制将為對象内所包含的所有對象進行遞歸複制。是以,deepcopy的結果是生成一個與源對象完全一樣,但互相獨立的對象,生成的對象與源對象不共享任何東西。在smalltalk中,每個類都獲得copy方法,而且copy的預設實作就是shallowcopy。需要組合使用shallowcopy和deepcopy的類(很多情況都需要這樣)應該實作自己的copy方法。smalltalk按值傳遞對象的語義和c++中傳遞引用幾乎等價。然而需要注意的是,一些最新的smalltalk實作(如visualworks)使用了略為不同的複制方案。

eiffel:

eiffel在複制對象時,遵循組合的引用—值(reference-value)語義。複制對象的方法稱為clone(),在預設情況下,所有類都可以使用(與create類似)。該成員函數對基本類型(如整數和字元)進行真正地複制,即複制它們的值。然而,如果原始對象包含對另一個對象的引用,則隻複制引用,而不是複制被引用的對象,這與淺複制十分類似。在本書其他章節提到過,在eiffel中,對象隻能包含基本類型或對其他對象的引用。對象不能按值包含另一個對象它隻能包含對其他對象的引用2(為了友善共享)。如果類的實作者需要一個不同的複制語義,必須在類中為其他成員函數也提供相應的複制語義。eiffel并不真正支援按值調用。在eiffel中,調用函數(或過程)将把形參與實參相關聯,這和c++中的引用類似。但不同的是,被調函數不能直接修改實參。換言之,對于任何arg參數,被調函數都不能修改它的值。如果arg是一個基本類型參數,則被調函數不能修改arg的值(它所引用的内容);如果arg是類類型,則被調函數不能将arg與新對象相關聯,或将其與void引用。但是,eiffel允許函數通過arg調用方法來修改arg。(不能把這種政策和c++的c<code>onst</code>成員函數混淆。在任何情況下,無論是直接還是間接,都不能修改<code>const</code>成員函數的對象。)鑒于eiffel的這個特性,函數隻可通過預定義的保留名稱result,才能向主調函數傳回值,result可以在函數内部(而不是在過程中)使用。

了解複制對象的另一種方法是将一個對象想象為樹的根節點。該對象(節點)可以包含任意數量的對其他對象(節點)的引用(在c++中,也包括任意數量的指向其他對象(節點)的指針)。當我們以樹的形式描繪對象圖時,它是一棵非常大(深)的樹。深複

《C++面向對象高效程式設計(第2版)》——4.5 對象複制的語義

圖4-9

制從樹的根節點開始,然後遞歸周遊整棵樹,複制每一個節點。複制操作結束後,我們獲得一棵新樹,它和原樹完全一樣。而淺複制隻能複制根節點,其他所有節點在原樹和副本樹之間共享,複制的結果隻是一棵帶共享節點的樹(見圖4-9)。

現在,我們來看看具有深複制語義的複制構造函數的實作:

繼續閱讀