第13章 拷貝控制
- 拷貝控制操作:拷貝構(gòu)造函數(shù)错览、拷貝賦值運(yùn)算符理澎、移動構(gòu)造函數(shù)逞力、移動賦值運(yùn)算符、析構(gòu)函數(shù)糠爬。
- 當(dāng)類中沒有聲明構(gòu)造函數(shù)時寇荧,編譯器會在其需要時生成合成默認(rèn)構(gòu)造函數(shù)。當(dāng)類中沒有定義拷貝構(gòu)造函數(shù)時执隧,編譯器生成合成拷貝構(gòu)造函數(shù)揩抡。合成拷貝賦值運(yùn)算符、合成析構(gòu)函數(shù)與合成拷貝構(gòu)造函數(shù)類似镀琉。當(dāng)類中沒有自定義拷貝控制成員峦嗤,且每個非static數(shù)據(jù)成員都可以移動時,編譯器才會合成移動構(gòu)造函數(shù)或移動賦值運(yùn)算符屋摔。
- 若一個類需要析構(gòu)函數(shù)烁设,則幾乎肯定需要拷貝構(gòu)造函數(shù)、拷貝賦值運(yùn)算符钓试;若一個類需要拷貝構(gòu)造函數(shù)装黑,則幾乎肯定需要拷貝賦值運(yùn)算符;若一個類需要拷貝賦值運(yùn)算符弓熏,則幾乎肯定需要拷貝構(gòu)造函數(shù)恋谭。若一個類定義任意一個拷貝控制,則應(yīng)該定義所有的5個拷貝控制操作挽鞠。
13.1 拷貝疚颊、賦值與銷毀
1. 拷貝構(gòu)造函數(shù)
- 拷貝構(gòu)造函數(shù)的第一個參數(shù)是自身類型的引用,且任意額外參數(shù)都有默認(rèn)值滞谢〈。拷貝構(gòu)造函數(shù)通常不是
explicit
,第一個參數(shù)幾乎總是const
狮杨。 - 直接初始化要求編譯器通過函數(shù)匹配選擇構(gòu)造函數(shù)母截,拷貝初始化要求編譯器將右側(cè)運(yùn)算對象拷貝到左側(cè)運(yùn)算對象,必要時可進(jìn)行類型轉(zhuǎn)換橄教。
- 使用拷貝初始化的情況:使用
=
定義變量清寇;將實(shí)參傳遞給非引用形參;返回類型為非引用類型的函數(shù)返回對象护蝶;花括號列表初始化數(shù)組元素或聚合類成員华烟;某些類類型會對其分配的對象進(jìn)行拷貝初始化,如vector
的insert
和push
進(jìn)行拷貝初始化持灰,emplace
進(jìn)行直接初始化盔夜。 - 拷貝構(gòu)造函數(shù)的第一個參數(shù)必須是引用類型,因?yàn)楹瘮?shù)調(diào)用過程中,非引用類型的形參通過拷貝構(gòu)造函數(shù)進(jìn)行拷貝初始化喂链。若第一個參數(shù)不是引用類型返十,函數(shù)調(diào)用時非引用類型的形參使用拷貝構(gòu)造函數(shù)初始化,而拷貝構(gòu)造函數(shù)的第一個參數(shù)是非引用類型椭微,第一個參數(shù)又需要調(diào)用拷貝構(gòu)造函數(shù)洞坑,如此會無限循環(huán)。
- 雖然編譯器可以略過拷貝/移動構(gòu)造函數(shù)蝇率,但依然要求拷貝/移動構(gòu)造函數(shù)必須存在且可訪問迟杂。
class Foo{
public:
Foo(); // 默認(rèn)構(gòu)造函數(shù)
Foo(const Foo&); // 拷貝構(gòu)造函數(shù)
}
string s = "1"; // 拷貝初始化,等價于string temp("1"); string s = temp; //使用拷貝構(gòu)造函數(shù)
string s("1"); // "1":const char *本慕,略過拷貝構(gòu)造函數(shù)
2. 拷貝賦值運(yùn)算符
- 賦值運(yùn)算符通常返回一個指向其左側(cè)運(yùn)算對象的引用排拷。標(biāo)準(zhǔn)庫通常要求保存在容器中的類型要具有賦值運(yùn)算符,且其返回值是左側(cè)運(yùn)算對象的引用间狂。
- 大多數(shù)賦值運(yùn)算符會結(jié)合析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù)的工作攻泼。編寫賦值運(yùn)算符時需注意自賦值情況,最好是在銷毀左側(cè)運(yùn)算對象資源之前拷貝右側(cè)運(yùn)算對象鉴象。
3. 析構(gòu)函數(shù)
- 在構(gòu)造函數(shù)中,成員初始化在函數(shù)體之前完成何鸡,且按照它們在類中出現(xiàn)的順序初始化纺弊。在析構(gòu)函數(shù)中,先執(zhí)行函數(shù)體骡男,再按照初始化順序的逆序銷毀成員淆游。
- 若成員是內(nèi)置指針類型,析構(gòu)函數(shù)不會自動delete其所指的對象隔盛。若成員是智能指針犹菱,因智能指針是類類型,故會執(zhí)行類成員自己的析構(gòu)函數(shù)實(shí)現(xiàn)自動銷毀吮炕。
- 調(diào)用析構(gòu)函數(shù)的情況:變量離開作用域時被銷毀腊脱;當(dāng)一個對象被銷毀時,其成員被銷毀龙亲;標(biāo)準(zhǔn)庫容器或數(shù)組被銷毀時陕凹,其元素被銷毀;對于動態(tài)分配的對象鳄炉,當(dāng)delete指向該對象的指針時被銷毀杜耙;對于臨時對象,當(dāng)創(chuàng)建它的完整表達(dá)式結(jié)束時被銷毀拂盯。
- 當(dāng)指向一個對象的引用或指針離開作用域時不會執(zhí)行析構(gòu)函數(shù)佑女。
4. =default
和=delete
- 使用
=default
可顯式要求編譯器生成合成版本的拷貝控制成員。在類內(nèi)使用=default
,合成的函數(shù)是內(nèi)聯(lián)的团驱,在類外使用=default
摸吠,合成的函數(shù)就不是內(nèi)聯(lián)的。只能對具有合成版本的成員函數(shù)(默認(rèn)構(gòu)造函數(shù)店茶,拷貝控制成員)使用=default
蜕便。 - 使用
=delete
表示不能以任意方式調(diào)用該成員函數(shù)。=delete
必須出現(xiàn)在函數(shù)第一次聲明的時候贩幻〗蜗伲可對任意函數(shù)使用=delete
,不局限于默認(rèn)構(gòu)造函數(shù)和拷貝控制成員丛楚。 - 最好不要對析構(gòu)函數(shù)使用
=delete
族壳。對于析構(gòu)函數(shù)已刪除的類型,不能定義該類型的變量趣些,可動態(tài)分配但不能釋放該類型的對象仿荆。 - 將拷貝控制成員聲明為
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 拷貝控制和資源管理
- 可定義拷貝操作使類的行為看起來像一個值或一個指針混滔。類的行為像一個值,意味著類有自己的狀態(tài),副本與原對象無關(guān)坯屿,修改副本不會改變原對象油湖。類的行為像一個指針,則類共享狀態(tài)领跛,副本與原對象使用相同底層數(shù)據(jù)乏德,修改副本會改變原對象。
- 指針成員的拷貝決定類的行為像值或像指針吠昭。
- 令類的行為像指針喊括,最好是使用
share_ptr
管理類中的資源。若想直接管理資源矢棚,則需使用引用計(jì)數(shù)郑什。可將引用計(jì)數(shù)保存在動態(tài)內(nèi)存中蒲肋。
13.3 交換操作
- 當(dāng)作用域有
using std::swap
蘑拯,若存在類型特定的swap
版本,swap
調(diào)用會與之匹配兜粘,若不存在類型特定版本申窘,則會使用std::swap
- 對于行為類值的類,賦值運(yùn)算符通過拷貝并交換技術(shù)(形參是非引用類型孔轴,
swap
定義賦值運(yùn)算符)可自動處理自賦值情況且天然就是異常安全的偶洋。
13.4 拷貝控制示例
- 當(dāng)類需要分配資源、簿記工作(類似于郵件處理應(yīng)用中的
Message
和Folder
)等操作時距糖,通常需要拷貝控制。
13.5 動態(tài)內(nèi)存管理類
- 當(dāng)類需要在運(yùn)行時分配可變大小的內(nèi)存空間時牵寺,通常使用標(biāo)準(zhǔn)庫容器保存其數(shù)據(jù)悍引。
- 若類需要自己進(jìn)行內(nèi)存分配,則必須定義自己的拷貝控制成員來管理所分配的內(nèi)存帽氓。
13.6 對象移動
- 移動而非拷貝對象的情況:對象拷貝后就立即被銷毀趣斤;
IO
、unique_ptr
等類中包含不能被共享的資源(如IO緩沖黎休、指針)浓领。 - 標(biāo)準(zhǔn)庫容器、
string
和shared_ptr
類同時支持拷貝和移動势腮,IO
和unique_ptr
類只支持移動联贩。
1. 左值引用
- 右值引用
&&
即必須綁定到右值的引用。右值引用只能綁定到一個將要銷毀的對象捎拯。 - 左值和右值是表達(dá)式的屬性泪幌,左值表達(dá)式表示一個對象的身份,右值表達(dá)式表示對象的值。左值有持久的狀態(tài)祸泪,右值只能是字面常量或表達(dá)式求值過程中創(chuàng)建的臨時對象吗浩。
- 非const左值引用可以綁定賦值、下標(biāo)没隘、解引用懂扼、前置遞增/遞減、返回左值引用的函數(shù)右蒲;const左值引用和右值引用可以綁定算術(shù)阀湿、關(guān)系、位品嚣、后置遞增/遞減炕倘、要求轉(zhuǎn)換的表達(dá)式、字面常量翰撑、返回右值的表達(dá)式罩旋。
-
std::move
可將左值轉(zhuǎn)換為對應(yīng)的右值引用類型搔弄。移后源對象(使用std::move
后的對象)可以被銷毀或賦值蚊逢,但不能使用其值。
int i = 42;
int &r1 = i;
const int &r2 = 42;
int && r3 = 42;
int &&r4 = r3; // 錯誤郁季,r3是變量逝撬,變量是左值
int &&r5 = std::move(i);
2. 移動構(gòu)造函數(shù)和移動賦值運(yùn)算符
- 移動構(gòu)造函數(shù)第一個參數(shù)是非const的右值引用浴骂,任何額外參數(shù)必須有默認(rèn)實(shí)參。移動構(gòu)造函數(shù)需完成資源移動宪潮,保證移后源對象可被銷毀和賦值溯警。
- 不拋出異常的移動構(gòu)造函數(shù)和移動賦值函數(shù)必須標(biāo)記
noexcept
。 - 當(dāng)類中沒有自定義拷貝控制成員狡相,且每個非static數(shù)據(jù)成員都可以移動時梯轻,編譯器才會合成移動構(gòu)造函數(shù)或移動賦值運(yùn)算符。
- 定義了一個移動構(gòu)造函數(shù)或移動賦值運(yùn)算符的類必須定義自己的拷貝操作尽棕,否則合成拷貝構(gòu)造函數(shù)和合成拷貝賦值運(yùn)算符會被定義為刪除的喳挑。
- 若類中拷貝操作與移動操作同時存在,則進(jìn)行函數(shù)匹配滔悉,實(shí)參是左值的函數(shù)會使用拷貝操作伊诵,實(shí)參是右值的函數(shù)會使用移動操作。若類中只有拷貝操作且實(shí)參是右值回官,則會調(diào)用拷貝操作曹宴。
-
make_move_iterator
可將普通迭代器轉(zhuǎn)換為移動迭代器。 - 不要隨便使用移動操作孙乖。移后源對象具有不確定的狀態(tài)浙炼,對其調(diào)用
std::move
很危險份氧。當(dāng)我們調(diào)用move
時,必須絕對確認(rèn)移后源對象沒有其它用戶弯屈。
=delete |
原因 |
---|---|
移動構(gòu)造函數(shù) | 1蜗帜、類成員定義自己的拷貝構(gòu)造函數(shù)且未定義移動構(gòu)造函數(shù) 2、類成員未定義自己的拷貝構(gòu)造函數(shù)且編譯器不能合成移動構(gòu)造函數(shù) 3资厉、類成員的移動構(gòu)造函數(shù)被定義為刪除的或不可訪問的 4厅缺、類的析構(gòu)函數(shù)被定義為刪除的或不可訪問的 |
移動賦值運(yùn)算符 | 1、類成員定義自己的拷貝賦值運(yùn)算符且未定義移動賦值運(yùn)算符 2宴偿、類成員未定義自己的拷貝賦值運(yùn)算符且編譯器不能合成移動賦值運(yùn)算符 3湘捎、類成員的移動賦值運(yùn)算符被定義為刪除的或不可訪問的 4、類成員是 const 或引用 |
3. 右值引用和成員函數(shù)
- 除構(gòu)造函數(shù)和賦值運(yùn)算符外窄刘,其它成員函數(shù)也可同時提供拷貝和移動版本窥妇,拷貝版本接受一個指向const的左值引用,移動版本接受一個指向非const的右值引用娩践。
- 通常我們可以在一個對象上調(diào)用成員活翩,而不用管該對象是左值或右值。C++允許向右值賦值翻伺,若想阻止該用法材泄,強(qiáng)制使左側(cè)運(yùn)算對象必須是左值,可在參數(shù)列表后放置引用限定符吨岭。
(s1+s2) = "abc"; // 雖然s1+s2返回右值拉宗,但它依舊可以調(diào)用拷貝賦值運(yùn)算符來實(shí)現(xiàn)賦值。
- 引用限定符可以是
&
或&&
辣辫,&
指出this
可以指向一個左值旦事,&&
指出this
可以指向一個右值。若const
與引用限定符同時存在急灭,則const
必須在前族檬,引用限定符必須在后。 - 若一個成員函數(shù)有引用限定符化戳,則具有相同參數(shù)列表的所有版本都必須有引用限定符。