復制構造函數。
與默認構造函數一樣,復制構造函數可由編譯器隱式調用。
復制構造函數可用於:
根據另一個同類型的對象顯式或隱式初始化一個對象。
復制一個對象,將它作為實參傳給一個函數。
從函數返回時復制一個對象。
初始化順序容器中的元素。
根據元素初始化式列表初始化數組元素。
對象的定義形式
回憶一下,C++ 支持兩種初始化形式:
直接初始化和復制初始化。復制初始化使用 = 符號,而直接初始化將初始化式放在圓括號中。
當用於類類型對象時,初始化的復制形式和直接形式有所不同:
直接初始化直接調用與實參匹配的構造函數,復制初始化總是調用復制構造函數。
復制初始化首先使用指定構造函數創建一個臨時對象,然後用復制構造函數將那個臨時對象復制到正在創建的對象:
null_book = ; dots(, ); empty_copy = (); empty_direct;
對於類類型對象,只有指定單個實參或顯式創建一個臨時對象用於復制時,才使用復制初始化。
創建 dots 時,調用參數為一個數量和一個字符的 string 構造函數並直接初始化 dots 的成員。
創建 null_book 時,編譯器首先調用接受一個 C 風格字符串形參的 string 構造函數,創建一個臨時對象,
然後,編譯器使用 string 復制構造函數將 null_book 初始化為那個臨時對象的副本。
empty_copy 和 empty_direct 的初始化都調用默認構造函數。
對前者初始化時,默認構造函數函數創建一個臨時對象,然後復制構造函數用該對象初始化 empty_copy。
對後者初始化時,直接運行 empty_direct 的默認構造函數。
支持初始化的復制形式主要是為了與 C 的用法兼容。
當情況許可時,可以允許編譯器跳過復制構造函數直接創建對象,但編譯器沒有義務這樣做。
通常直接初始化和復制初始化僅在低級別上存在差異。
然而,對於不支持復制的類型,或者使用非 explicit 構造函數的時候,它們有本質區別:
ifstream file1(); ifstream file2 = ; Sales_item item = ();
ifstream 類定義了一個可用 C 風格字符串調用的構造函數,使用該構造函數初始化 file1。
看上去等效的 file2 初始化使用復制初始化,但該定義不正確。
由於不能復制 IO 類型的對象,所以不能對那些類型的對象使用復制初始化。
item 的初始化是否正確,取決於正在使用哪個版本的 Sales_item 類。
某些版本將參數為一個 string 的構造函數定義為 explicit。
如果構造函數是顯式的,則初始化失敗;如果構造函數不是顯式的,則初始化成功。
形參與返回值
正如我們所知,當形參為非引用類型的時候,將復制實參的值。
類似地,以非引用類型作返回值時,將返回 return 語句 中的值的副本。
當形參或返回值為類類型時,由復制構造函數進行復制。例如,考慮之前寫過的 make_plural 函數:
make_plural(size_t, &, &);
這個函數隱式使用 string 復制構造函數返回給定單詞的復數形式。形參是 const 引用,不能復制。
初始化容器元素
復制構造函數可用於初始化順序容器中的元素。
例如,可以用表示容量的單個形參來初始化容器。
容器的這種構造方式使用默認構造函數和復制構造函數:
vector<> svec();
編譯器首先使用 string 默認構造函數創建一個臨時值來初始化 svec,
然後使用復制構造函數將臨時值復制到 svec 的每個元素。
作為一般規則,除非你想使用容器元素的默認初始值,
更有效的辦法是,分配一個空容器並將已知元素的值加入容器。
構造函數與數組元素
如果沒有為類類型數組提供元素初始化式,則將用默認構造函數初始化每個元素。
然而,如果使用常規的花括號括住的數組初始化列表來提供顯式元素初始化式,則使用復制初始化來初始化每個元素。
根據指定值創建適當類型的元素,然後用復制構造函數將該值復制到相應元素:
Sales_item primer_eds[] = { (((
如前三個元素的初始化式中所示可以直接指定一個值,用於調用元素類型的單實參構造函數。
如果希望不指定實參或指定多個實參,就需要使用完整的構造函數語法,正如最後一個元素的初始化那樣。
如果我們沒有定義復制構造函數,編譯器就會為我們合成一個。
與合成的默認構造函數不同,即使我們定義了其他構造函數,也會合成復制構造函數。
合成復制構造函數的行為是,
所謂“逐個成員”,指的是編譯器將現在對象的每個非 static 成員,依次復制到正創建的對象。
只有一個例外,每個成員類型決定了復制該成員的含義。
合成復制構造函數直接復制內置類型成員的值,類類型成員使用該類的復制構造函數進行復制。
數組成員的復制是個例外。
雖然一般不能復制數組,但如果一個類具有數組成員,則合成復制構造函數將復制數組。
復制數組時合成復制構造函數將復制數組的每一個元素。
逐個成員初始化最簡單的概念模型是,
例如,對於我們的 Sales_item 類,它有三個數據成員:
Sales_item & units_sold(orig.units_sold), revenue(orig.revenue) { }
復制構造函數就是接受單個類類型引用形參(通常用 const 修飾)的構造函數:
Foo( Foo&); };
雖然也可以定義接受非 const 引用的復制構造函數,但形參通常是一個 const 引用。
因為用於向函數傳遞對象和從函數返回對象,該構造函數一般不應設置為 explicit。
復制構造函數應將實參的成員復制到正在構造的對象。
對許多類而言,合成復制構造函數只完成必要的工作。
只包含類類型成員或內置類型(但不是指針類型)成員的類,無須顯式地定義復制構造函數,也可以復制。
然而,有些類必須對復制對象時發生的事情加以控制。
這樣的類經常有一個數據成員是指針,或者有成員表示在構造函數中分配的其他資源。
而另一些類在創建新對象時必須做一些特定工作。這兩種情況下,都必須定義復制構造函數。
通常,定義復制構造函數最困難的部分在於認識到需要復制構造函數。
只要能認識到需要復制構造函數,定義構造函數一般非常簡單。
復制構造函數的定義與其他構造函數一樣:它與類同名,沒有返回值,
可以(而且應該)使用構造函數初始化列表初始化新創建對象的成員,可以在函數體中做任何其他必要工作。
後續章節中將給出一些需要定義復制構造函數的類的例子。
有些類需要完全禁止復制。例如,iostream 類就不允許復制。
如果想要禁止復制,似乎可以省略復制構造函數,然而,如果不定義復制構造函數,編譯器將合成一個。
如果復制構造函數是私有的,將不允許用戶代碼復制該類類型的對象,編譯器將拒絕任何進行復制的嘗試。
然而,類的友元和成員仍可以進行復制。
聲明一個(private)復制構造函數但不對其定義。
聲明而不定義成員函數是合法的,但是,使用未定義成員的任何嘗試將導致鏈接失敗。
通過聲明(但不定義)private 復制構造函數,可以禁止任何復制類類型對象的嘗試:
用戶代碼中復制嘗試將在編譯時標記為錯誤,而成員函數和友元中的復制嘗試將在鏈接時導致錯誤。
不定義復制構造函數和/或默認構造函數,會嚴重局限類的使用。
不允許復制的類對象只能作為引用傳遞給函數或從函數返回,它們也不能用作容器的元素。
一般來說,最好顯式或隱式定義默認構造函數和復制構造函數。
只有不存在其他構造函數時才合成默認構造函數。
如果定義了復制構造函數,也必須定義默認構造函數。