第13章 拷貝控制
- 拷貝控制操作:拷貝構(gòu)造函數(shù)、拷貝賦值運(yùn)算符、移動(dòng)構(gòu)造函數(shù)、移動(dòng)賦值運(yùn)算符、析構(gòu)函數(shù)。
- 當(dāng)類中沒有聲明構(gòu)造函數(shù)時(shí),編譯器會(huì)在其需要時(shí)生成合成默認(rèn)構(gòu)造函數(shù)。當(dāng)類中沒有定義拷貝構(gòu)造函數(shù)時(shí),編譯器生成合成拷貝構(gòu)造函數(shù)。合成拷貝賦值運(yùn)算符、合成析構(gòu)函數(shù)與合成拷貝構(gòu)造函數(shù)類似。當(dāng)類中沒有自定義拷貝控制成員,且每個(gè)非static數(shù)據(jù)成員都可以移動(dòng)時(shí),編譯器才會(huì)合成移動(dòng)構(gòu)造函數(shù)或移動(dòng)賦值運(yùn)算符。
- 若一個(gè)類需要析構(gòu)函數(shù),則幾乎肯定需要拷貝構(gòu)造函數(shù)、拷貝賦值運(yùn)算符;若一個(gè)類需要拷貝構(gòu)造函數(shù),則幾乎肯定需要拷貝賦值運(yùn)算符;若一個(gè)類需要拷貝賦值運(yùn)算符,則幾乎肯定需要拷貝構(gòu)造函數(shù)。若一個(gè)類定義任意一個(gè)拷貝控制,則應(yīng)該定義所有的5個(gè)拷貝控制操作。
13.1 拷貝、賦值與銷毀
1. 拷貝構(gòu)造函數(shù)
- 拷貝構(gòu)造函數(shù)的第一個(gè)參數(shù)是自身類型的引用,且任意額外參數(shù)都有默認(rèn)值。拷貝構(gòu)造函數(shù)通常不是
explicit,第一個(gè)參數(shù)幾乎總是const。 - 直接初始化要求編譯器通過函數(shù)匹配選擇構(gòu)造函數(shù),拷貝初始化要求編譯器將右側(cè)運(yùn)算對(duì)象拷貝到左側(cè)運(yùn)算對(duì)象,必要時(shí)可進(jìn)行類型轉(zhuǎn)換。
- 使用拷貝初始化的情況:使用
=定義變量;將實(shí)參傳遞給非引用形參;返回類型為非引用類型的函數(shù)返回對(duì)象;花括號(hào)列表初始化數(shù)組元素或聚合類成員;某些類類型會(huì)對(duì)其分配的對(duì)象進(jìn)行拷貝初始化,如vector的insert和push進(jìn)行拷貝初始化,emplace進(jìn)行直接初始化。 - 拷貝構(gòu)造函數(shù)的第一個(gè)參數(shù)必須是引用類型,因?yàn)楹瘮?shù)調(diào)用過程中,非引用類型的形參通過拷貝構(gòu)造函數(shù)進(jìn)行拷貝初始化。若第一個(gè)參數(shù)不是引用類型,函數(shù)調(diào)用時(shí)非引用類型的形參使用拷貝構(gòu)造函數(shù)初始化,而拷貝構(gòu)造函數(shù)的第一個(gè)參數(shù)是非引用類型,第一個(gè)參數(shù)又需要調(diào)用拷貝構(gòu)造函數(shù),如此會(huì)無限循環(huán)。
- 雖然編譯器可以略過拷貝/移動(dòng)構(gòu)造函數(shù),但依然要求拷貝/移動(dòng)構(gòu)造函數(shù)必須存在且可訪問。
class Foo{
public:
Foo(); // 默認(rèn)構(gòu)造函數(shù)
Foo(const Foo&); // 拷貝構(gòu)造函數(shù)
}
string s = "1"; // 拷貝初始化,等價(jià)于string temp("1"); string s = temp; //使用拷貝構(gòu)造函數(shù)
string s("1"); // "1":const char *,略過拷貝構(gòu)造函數(shù)
2. 拷貝賦值運(yùn)算符
- 賦值運(yùn)算符通常返回一個(gè)指向其左側(cè)運(yùn)算對(duì)象的引用。標(biāo)準(zhǔn)庫通常要求保存在容器中的類型要具有賦值運(yùn)算符,且其返回值是左側(cè)運(yùn)算對(duì)象的引用。
- 大多數(shù)賦值運(yùn)算符會(huì)結(jié)合析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù)的工作。編寫賦值運(yùn)算符時(shí)需注意自賦值情況,最好是在銷毀左側(cè)運(yùn)算對(duì)象資源之前拷貝右側(cè)運(yùn)算對(duì)象。
3. 析構(gòu)函數(shù)
- 在構(gòu)造函數(shù)中,成員初始化在函數(shù)體之前完成,且按照它們?cè)陬愔谐霈F(xiàn)的順序初始化。在析構(gòu)函數(shù)中,先執(zhí)行函數(shù)體,再按照初始化順序的逆序銷毀成員。
- 若成員是內(nèi)置指針類型,析構(gòu)函數(shù)不會(huì)自動(dòng)delete其所指的對(duì)象。若成員是智能指針,因智能指針是類類型,故會(huì)執(zhí)行類成員自己的析構(gòu)函數(shù)實(shí)現(xiàn)自動(dòng)銷毀。
- 調(diào)用析構(gòu)函數(shù)的情況:變量離開作用域時(shí)被銷毀;當(dāng)一個(gè)對(duì)象被銷毀時(shí),其成員被銷毀;標(biāo)準(zhǔn)庫容器或數(shù)組被銷毀時(shí),其元素被銷毀;對(duì)于動(dòng)態(tài)分配的對(duì)象,當(dāng)delete指向該對(duì)象的指針時(shí)被銷毀;對(duì)于臨時(shí)對(duì)象,當(dāng)創(chuàng)建它的完整表達(dá)式結(jié)束時(shí)被銷毀。
- 當(dāng)指向一個(gè)對(duì)象的引用或指針離開作用域時(shí)不會(huì)執(zhí)行析構(gòu)函數(shù)。
4. =default和=delete
- 使用
=default可顯式要求編譯器生成合成版本的拷貝控制成員。在類內(nèi)使用=default,合成的函數(shù)是內(nèi)聯(lián)的,在類外使用=default,合成的函數(shù)就不是內(nèi)聯(lián)的。只能對(duì)具有合成版本的成員函數(shù)(默認(rèn)構(gòu)造函數(shù),拷貝控制成員)使用=default。 - 使用
=delete表示不能以任意方式調(diào)用該成員函數(shù)。=delete必須出現(xiàn)在函數(shù)第一次聲明的時(shí)候??蓪?duì)任意函數(shù)使用=delete,不局限于默認(rèn)構(gòu)造函數(shù)和拷貝控制成員。 - 最好不要對(duì)析構(gòu)函數(shù)使用
=delete。對(duì)于析構(gòu)函數(shù)已刪除的類型,不能定義該類型的變量,可動(dòng)態(tài)分配但不能釋放該類型的對(duì)象。 - 將拷貝控制成員聲明為
private但不定義,可阻止用戶代碼、友元函數(shù)、成員函數(shù)進(jìn)行拷貝控制。
=delete |
原因 |
|---|---|
| 合成默認(rèn)構(gòu)造函數(shù) | 1、類成員的析構(gòu)函數(shù)是刪除(=delete)或不可訪問(private)。2、類中含有引用成員,該成員沒有類內(nèi)初始化器。 3、類中含有const成員,該成員沒有類內(nèi)初始化器,且其類型未顯式定義默認(rèn)構(gòu)造函數(shù)。 |
| 合成拷貝構(gòu)造函數(shù) | 1、類成員的拷貝構(gòu)造函數(shù)是刪除或不可訪問。 2、類成員的析構(gòu)函數(shù)是刪除或不可訪問。 |
| 合成拷貝賦值運(yùn)算符 | 1、類成員的拷貝賦值運(yùn)算符是刪除或不可訪問。 2、類中含有const成員或引用成員。 |
| 合成析構(gòu)函數(shù) | 1、類成員的析構(gòu)函數(shù)是刪除或不可訪問。 |
13.2 拷貝控制和資源管理
- 可定義拷貝操作使類的行為看起來像一個(gè)值或一個(gè)指針。類的行為像一個(gè)值,意味著類有自己的狀態(tài),副本與原對(duì)象無關(guān),修改副本不會(huì)改變?cè)瓕?duì)象。類的行為像一個(gè)指針,則類共享狀態(tài),副本與原對(duì)象使用相同底層數(shù)據(jù),修改副本會(huì)改變?cè)瓕?duì)象。
- 指針成員的拷貝決定類的行為像值或像指針。
- 令類的行為像指針,最好是使用
share_ptr管理類中的資源。若想直接管理資源,則需使用引用計(jì)數(shù)??蓪⒁糜?jì)數(shù)保存在動(dòng)態(tài)內(nèi)存中。
13.3 交換操作
- 當(dāng)作用域有
using std::swap,若存在類型特定的swap版本,swap調(diào)用會(huì)與之匹配,若不存在類型特定版本,則會(huì)使用std::swap - 對(duì)于行為類值的類,賦值運(yùn)算符通過拷貝并交換技術(shù)(形參是非引用類型,
swap定義賦值運(yùn)算符)可自動(dòng)處理自賦值情況且天然就是異常安全的。
13.4 拷貝控制示例
- 當(dāng)類需要分配資源、簿記工作(類似于郵件處理應(yīng)用中的
Message和Folder)等操作時(shí),通常需要拷貝控制。

13.5 動(dòng)態(tài)內(nèi)存管理類
- 當(dāng)類需要在運(yùn)行時(shí)分配可變大小的內(nèi)存空間時(shí),通常使用標(biāo)準(zhǔn)庫容器保存其數(shù)據(jù)。
- 若類需要自己進(jìn)行內(nèi)存分配,則必須定義自己的拷貝控制成員來管理所分配的內(nèi)存。
13.6 對(duì)象移動(dòng)
- 移動(dòng)而非拷貝對(duì)象的情況:對(duì)象拷貝后就立即被銷毀;
IO、unique_ptr等類中包含不能被共享的資源(如IO緩沖、指針)。 - 標(biāo)準(zhǔn)庫容器、
string和shared_ptr類同時(shí)支持拷貝和移動(dòng),IO和unique_ptr類只支持移動(dòng)。
1. 左值引用
- 右值引用
&&即必須綁定到右值的引用。右值引用只能綁定到一個(gè)將要銷毀的對(duì)象。 - 左值和右值是表達(dá)式的屬性,左值表達(dá)式表示一個(gè)對(duì)象的身份,右值表達(dá)式表示對(duì)象的值。左值有持久的狀態(tài),右值只能是字面常量或表達(dá)式求值過程中創(chuàng)建的臨時(shí)對(duì)象。
- 非const左值引用可以綁定賦值、下標(biāo)、解引用、前置遞增/遞減、返回左值引用的函數(shù);const左值引用和右值引用可以綁定算術(shù)、關(guān)系、位、后置遞增/遞減、要求轉(zhuǎn)換的表達(dá)式、字面常量、返回右值的表達(dá)式。
-
std::move可將左值轉(zhuǎn)換為對(duì)應(yīng)的右值引用類型。移后源對(duì)象(使用std::move后的對(duì)象)可以被銷毀或賦值,但不能使用其值。
int i = 42;
int &r1 = i;
const int &r2 = 42;
int && r3 = 42;
int &&r4 = r3; // 錯(cuò)誤,r3是變量,變量是左值
int &&r5 = std::move(i);
2. 移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值運(yùn)算符
- 移動(dòng)構(gòu)造函數(shù)第一個(gè)參數(shù)是非const的右值引用,任何額外參數(shù)必須有默認(rèn)實(shí)參。移動(dòng)構(gòu)造函數(shù)需完成資源移動(dòng),保證移后源對(duì)象可被銷毀和賦值。
- 不拋出異常的移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù)必須標(biāo)記
noexcept。 - 當(dāng)類中沒有自定義拷貝控制成員,且每個(gè)非static數(shù)據(jù)成員都可以移動(dòng)時(shí),編譯器才會(huì)合成移動(dòng)構(gòu)造函數(shù)或移動(dòng)賦值運(yùn)算符。
- 定義了一個(gè)移動(dòng)構(gòu)造函數(shù)或移動(dòng)賦值運(yùn)算符的類必須定義自己的拷貝操作,否則合成拷貝構(gòu)造函數(shù)和合成拷貝賦值運(yùn)算符會(huì)被定義為刪除的。
- 若類中拷貝操作與移動(dòng)操作同時(shí)存在,則進(jìn)行函數(shù)匹配,實(shí)參是左值的函數(shù)會(huì)使用拷貝操作,實(shí)參是右值的函數(shù)會(huì)使用移動(dòng)操作。若類中只有拷貝操作且實(shí)參是右值,則會(huì)調(diào)用拷貝操作。
-
make_move_iterator可將普通迭代器轉(zhuǎn)換為移動(dòng)迭代器。 - 不要隨便使用移動(dòng)操作。移后源對(duì)象具有不確定的狀態(tài),對(duì)其調(diào)用
std::move很危險(xiǎn)。當(dāng)我們調(diào)用move時(shí),必須絕對(duì)確認(rèn)移后源對(duì)象沒有其它用戶。
=delete |
原因 |
|---|---|
| 移動(dòng)構(gòu)造函數(shù) | 1、類成員定義自己的拷貝構(gòu)造函數(shù)且未定義移動(dòng)構(gòu)造函數(shù) 2、類成員未定義自己的拷貝構(gòu)造函數(shù)且編譯器不能合成移動(dòng)構(gòu)造函數(shù) 3、類成員的移動(dòng)構(gòu)造函數(shù)被定義為刪除的或不可訪問的 4、類的析構(gòu)函數(shù)被定義為刪除的或不可訪問的 |
| 移動(dòng)賦值運(yùn)算符 | 1、類成員定義自己的拷貝賦值運(yùn)算符且未定義移動(dòng)賦值運(yùn)算符 2、類成員未定義自己的拷貝賦值運(yùn)算符且編譯器不能合成移動(dòng)賦值運(yùn)算符 3、類成員的移動(dòng)賦值運(yùn)算符被定義為刪除的或不可訪問的 4、類成員是 const或引用 |
3. 右值引用和成員函數(shù)
- 除構(gòu)造函數(shù)和賦值運(yùn)算符外,其它成員函數(shù)也可同時(shí)提供拷貝和移動(dòng)版本,拷貝版本接受一個(gè)指向const的左值引用,移動(dòng)版本接受一個(gè)指向非const的右值引用。
- 通常我們可以在一個(gè)對(duì)象上調(diào)用成員,而不用管該對(duì)象是左值或右值。C++允許向右值賦值,若想阻止該用法,強(qiáng)制使左側(cè)運(yùn)算對(duì)象必須是左值,可在參數(shù)列表后放置引用限定符。
(s1+s2) = "abc"; // 雖然s1+s2返回右值,但它依舊可以調(diào)用拷貝賦值運(yùn)算符來實(shí)現(xiàn)賦值。
- 引用限定符可以是
&或&&,&指出this可以指向一個(gè)左值,&&指出this可以指向一個(gè)右值。若const與引用限定符同時(shí)存在,則const必須在前,引用限定符必須在后。 - 若一個(gè)成員函數(shù)有引用限定符,則具有相同參數(shù)列表的所有版本都必須有引用限定符。