第13章 拷貝控制
- 拷貝控制操作:拷貝構造函數、拷貝賦值運算符、移動構造函數、移動賦值運算符、析構函數。
- 當類中沒有聲明構造函數時,編譯器會在其需要時生成合成默認構造函數。當類中沒有定義拷貝構造函數時,編譯器生成合成拷貝構造函數。合成拷貝賦值運算符、合成析構函數與合成拷貝構造函數類似。當類中沒有自定義拷貝控制成員,且每個非static數據成員都可以移動時,編譯器才會合成移動構造函數或移動賦值運算符。
- 若一個類需要析構函數,則幾乎肯定需要拷貝構造函數、拷貝賦值運算符;若一個類需要拷貝構造函數,則幾乎肯定需要拷貝賦值運算符;若一個類需要拷貝賦值運算符,則幾乎肯定需要拷貝構造函數。若一個類定義任意一個拷貝控制,則應該定義所有的5個拷貝控制操作。
13.1 拷貝、賦值與銷毀
1. 拷貝構造函數
- 拷貝構造函數的第一個參數是自身類型的引用,且任意額外參數都有默認值。拷貝構造函數通常不是
explicit
,第一個參數幾乎總是const
。 - 直接初始化要求編譯器通過函數匹配選擇構造函數,拷貝初始化要求編譯器將右側運算對象拷貝到左側運算對象,必要時可進行類型轉換。
- 使用拷貝初始化的情況:使用
=
定義變量;將實參傳遞給非引用形參;返回類型為非引用類型的函數返回對象;花括號列表初始化數組元素或聚合類成員;某些類類型會對其分配的對象進行拷貝初始化,如vector
的insert
和push
進行拷貝初始化,emplace
進行直接初始化。 - 拷貝構造函數的第一個參數必須是引用類型,因為函數調用過程中,非引用類型的形參通過拷貝構造函數進行拷貝初始化。若第一個參數不是引用類型,函數調用時非引用類型的形參使用拷貝構造函數初始化,而拷貝構造函數的第一個參數是非引用類型,第一個參數又需要調用拷貝構造函數,如此會無限循環。
- 雖然編譯器可以略過拷貝/移動構造函數,但依然要求拷貝/移動構造函數必須存在且可訪問。
class Foo{
public:
Foo(); // 默認構造函數
Foo(const Foo&); // 拷貝構造函數
}
string s = "1"; // 拷貝初始化,等價于string temp("1"); string s = temp; //使用拷貝構造函數
string s("1"); // "1":const char *,略過拷貝構造函數
2. 拷貝賦值運算符
- 賦值運算符通常返回一個指向其左側運算對象的引用。標準庫通常要求保存在容器中的類型要具有賦值運算符,且其返回值是左側運算對象的引用。
- 大多數賦值運算符會結合析構函數和拷貝構造函數的工作。編寫賦值運算符時需注意自賦值情況,最好是在銷毀左側運算對象資源之前拷貝右側運算對象。
3. 析構函數
- 在構造函數中,成員初始化在函數體之前完成,且按照它們在類中出現的順序初始化。在析構函數中,先執行函數體,再按照初始化順序的逆序銷毀成員。
- 若成員是內置指針類型,析構函數不會自動delete其所指的對象。若成員是智能指針,因智能指針是類類型,故會執行類成員自己的析構函數實現自動銷毀。
- 調用析構函數的情況:變量離開作用域時被銷毀;當一個對象被銷毀時,其成員被銷毀;標準庫容器或數組被銷毀時,其元素被銷毀;對于動態分配的對象,當delete指向該對象的指針時被銷毀;對于臨時對象,當創建它的完整表達式結束時被銷毀。
- 當指向一個對象的引用或指針離開作用域時不會執行析構函數。
4. =default
和=delete
- 使用
=default
可顯式要求編譯器生成合成版本的拷貝控制成員。在類內使用=default
,合成的函數是內聯的,在類外使用=default
,合成的函數就不是內聯的。只能對具有合成版本的成員函數(默認構造函數,拷貝控制成員)使用=default
。 - 使用
=delete
表示不能以任意方式調用該成員函數。=delete
必須出現在函數第一次聲明的時候。可對任意函數使用=delete
,不局限于默認構造函數和拷貝控制成員。 - 最好不要對析構函數使用
=delete
。對于析構函數已刪除的類型,不能定義該類型的變量,可動態分配但不能釋放該類型的對象。 - 將拷貝控制成員聲明為
private
但不定義,可阻止用戶代碼、友元函數、成員函數進行拷貝控制。
=delete |
原因 |
---|---|
合成默認構造函數 | 1、類成員的析構函數是刪除(=delete )或不可訪問(private )。2、類中含有引用成員,該成員沒有類內初始化器。 3、類中含有const成員,該成員沒有類內初始化器,且其類型未顯式定義默認構造函數。 |
合成拷貝構造函數 | 1、類成員的拷貝構造函數是刪除或不可訪問。 2、類成員的析構函數是刪除或不可訪問。 |
合成拷貝賦值運算符 | 1、類成員的拷貝賦值運算符是刪除或不可訪問。 2、類中含有const成員或引用成員。 |
合成析構函數 | 1、類成員的析構函數是刪除或不可訪問。 |
13.2 拷貝控制和資源管理
- 可定義拷貝操作使類的行為看起來像一個值或一個指針。類的行為像一個值,意味著類有自己的狀態,副本與原對象無關,修改副本不會改變原對象。類的行為像一個指針,則類共享狀態,副本與原對象使用相同底層數據,修改副本會改變原對象。
- 指針成員的拷貝決定類的行為像值或像指針。
- 令類的行為像指針,最好是使用
share_ptr
管理類中的資源。若想直接管理資源,則需使用引用計數。可將引用計數保存在動態內存中。
13.3 交換操作
- 當作用域有
using std::swap
,若存在類型特定的swap
版本,swap
調用會與之匹配,若不存在類型特定版本,則會使用std::swap
- 對于行為類值的類,賦值運算符通過拷貝并交換技術(形參是非引用類型,
swap
定義賦值運算符)可自動處理自賦值情況且天然就是異常安全的。
13.4 拷貝控制示例
- 當類需要分配資源、簿記工作(類似于郵件處理應用中的
Message
和Folder
)等操作時,通常需要拷貝控制。
13.5 動態內存管理類
- 當類需要在運行時分配可變大小的內存空間時,通常使用標準庫容器保存其數據。
- 若類需要自己進行內存分配,則必須定義自己的拷貝控制成員來管理所分配的內存。
13.6 對象移動
- 移動而非拷貝對象的情況:對象拷貝后就立即被銷毀;
IO
、unique_ptr
等類中包含不能被共享的資源(如IO緩沖、指針)。 - 標準庫容器、
string
和shared_ptr
類同時支持拷貝和移動,IO
和unique_ptr
類只支持移動。
1. 左值引用
- 右值引用
&&
即必須綁定到右值的引用。右值引用只能綁定到一個將要銷毀的對象。 - 左值和右值是表達式的屬性,左值表達式表示一個對象的身份,右值表達式表示對象的值。左值有持久的狀態,右值只能是字面常量或表達式求值過程中創建的臨時對象。
- 非const左值引用可以綁定賦值、下標、解引用、前置遞增/遞減、返回左值引用的函數;const左值引用和右值引用可以綁定算術、關系、位、后置遞增/遞減、要求轉換的表達式、字面常量、返回右值的表達式。
-
std::move
可將左值轉換為對應的右值引用類型。移后源對象(使用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. 移動構造函數和移動賦值運算符
- 移動構造函數第一個參數是非const的右值引用,任何額外參數必須有默認實參。移動構造函數需完成資源移動,保證移后源對象可被銷毀和賦值。
- 不拋出異常的移動構造函數和移動賦值函數必須標記
noexcept
。 - 當類中沒有自定義拷貝控制成員,且每個非static數據成員都可以移動時,編譯器才會合成移動構造函數或移動賦值運算符。
- 定義了一個移動構造函數或移動賦值運算符的類必須定義自己的拷貝操作,否則合成拷貝構造函數和合成拷貝賦值運算符會被定義為刪除的。
- 若類中拷貝操作與移動操作同時存在,則進行函數匹配,實參是左值的函數會使用拷貝操作,實參是右值的函數會使用移動操作。若類中只有拷貝操作且實參是右值,則會調用拷貝操作。
-
make_move_iterator
可將普通迭代器轉換為移動迭代器。 - 不要隨便使用移動操作。移后源對象具有不確定的狀態,對其調用
std::move
很危險。當我們調用move
時,必須絕對確認移后源對象沒有其它用戶。
=delete |
原因 |
---|---|
移動構造函數 | 1、類成員定義自己的拷貝構造函數且未定義移動構造函數 2、類成員未定義自己的拷貝構造函數且編譯器不能合成移動構造函數 3、類成員的移動構造函數被定義為刪除的或不可訪問的 4、類的析構函數被定義為刪除的或不可訪問的 |
移動賦值運算符 | 1、類成員定義自己的拷貝賦值運算符且未定義移動賦值運算符 2、類成員未定義自己的拷貝賦值運算符且編譯器不能合成移動賦值運算符 3、類成員的移動賦值運算符被定義為刪除的或不可訪問的 4、類成員是 const 或引用 |
3. 右值引用和成員函數
- 除構造函數和賦值運算符外,其它成員函數也可同時提供拷貝和移動版本,拷貝版本接受一個指向const的左值引用,移動版本接受一個指向非const的右值引用。
- 通常我們可以在一個對象上調用成員,而不用管該對象是左值或右值。C++允許向右值賦值,若想阻止該用法,強制使左側運算對象必須是左值,可在參數列表后放置引用限定符。
(s1+s2) = "abc"; // 雖然s1+s2返回右值,但它依舊可以調用拷貝賦值運算符來實現賦值。
- 引用限定符可以是
&
或&&
,&
指出this
可以指向一個左值,&&
指出this
可以指向一個右值。若const
與引用限定符同時存在,則const
必須在前,引用限定符必須在后。 - 若一個成員函數有引用限定符,則具有相同參數列表的所有版本都必須有引用限定符。