- 當定義一個類時,我們顯式地或隱式地指定在此類型的對象拷貝脸甘,移動恳啥,賦值和銷毀時做什么
- 一個類通過五種特殊的成員函數(shù)來控制這些操作,包括:拷貝構造函數(shù)丹诀,拷貝賦值運算符钝的,移動構造函數(shù)翁垂,移動賦值運算符 和 析構函數(shù)
- 拷貝和移動構造函數(shù)定義了當用同類型的另一個對象初始化本對象時做什么∠踝拷貝和移動賦值運算符定義了將一個對象賦予同類型的另一個對象時做什么沿猜。析構函數(shù)定義了當此類型對象銷毀時做什么
- 我們稱這些操作為拷貝控制操作
13.1 拷貝、賦值與銷毀
- 以最基本操作 拷貝構造函數(shù)碗脊,拷貝賦值運算符 和 析構函數(shù) 作為開始啼肩。移動操作在 13.6 節(jié)講述
13.1.1 拷貝構造函數(shù)
- 如果一個構造函數(shù)的第一個參數(shù)是自身類類型的引用,且任何額外參數(shù)都有默認值衙伶,則稱此構造函數(shù)是拷貝構造函數(shù)
拷貝初始化
- 當使用直接初始化時祈坠,我們實際上時要求編譯器使用普通的函數(shù)匹配來選擇與我們提供的參數(shù)最匹配的構造函數(shù)
- 當我們使用拷貝初始化時,我們要求編譯器將右側運算對象拷貝到正在創(chuàng)建的對象中矢劲,如果需要的話還要進行類型轉換
string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷貝初始化
string null_book = "9-999-99999-9"; // 拷貝初始化
string nines = string(100, '9'); // 拷貝初始化
參數(shù)和返回值
- 在函數(shù)調用過程中赦拘,具有非引用類型的參數(shù)要進行拷貝初始化
- 當一個函數(shù)具有非引用的返回類型時,返回值會被用來初始化調用方的結果
13.1.2 拷貝賦值運算符
- 與類控制其對象如何初始化一樣芬沉,類也可以控制其對象如何賦值
Sales_data trans, accum;
trans = accum; // 使用 Sales_data 的拷貝賦值運算符
- 與拷貝構造函數(shù)一樣躺同,如果類未定義自己的拷貝賦值運算符,編譯器會為它合成一個
重載賦值運算符
- 重載運算符本質上是函數(shù)丸逸,其名字由 operator 關鍵字后接表示要定義的運算符的符號組成蹋艺。因此,賦值運算符就是一個名為 operator= 的函數(shù)黄刚。類似于任何其他函數(shù)捎谨,運算符函數(shù)也有一個返回類型和一個參數(shù)列表
- 賦值運算符通常應該返回一個指向其左側運算對象的引用
class Foo{
public:
Foo& operator=(const Foo&); // 賦值運算符
// ....
};
A& operator= (const A& a){ //拷貝賦值運算符
val = a.val;
return *this;
}
- 與處理拷貝構造函數(shù)一樣,如果一個類未定義自己的拷貝賦值運算符隘击,編譯器會為它生成一個合成拷貝賦值運算符
13.1.3 析構函數(shù)
- 析構函數(shù)執(zhí)行與構造函數(shù)相反的操作:構造函數(shù)初始化對象的非 static 數(shù)據(jù)成員侍芝,還可能做一些其他工作;析構函數(shù)釋放對象使用的資源埋同,并銷毀對象的非 static 數(shù)據(jù)成員
- 析構函數(shù)是類的一個成員函數(shù),名字由波浪號接類名構成棵红。它沒有返回值也不接受參數(shù)
class Foo{
public:
~Foo(); // 析構函數(shù)
// ...
};
- 由于析構函數(shù)不接受參數(shù)凶赁,因此它不能被重載。對于一個給定類逆甜,只會有唯一一個析構函數(shù)
- 無論何時一個對象被銷毀虱肄,就會自動調用其析構函數(shù)
1, 變量在離開其作用域時被銷毀
2, 當一個對象被銷毀時,其成員被銷毀
3, 容器被銷毀時交煞,其元素被銷毀
4, 對于動態(tài)分配的對象咏窿,當對指向它的指針應用 delete 運算符時被銷毀
5, 對于臨時對象,當創(chuàng)建它的完整表達式結束時被銷毀
- 析構函數(shù)體自身并不直接銷毀成員素征。成員是在析構函數(shù)體之后隱含的析構階段中被銷毀的
13.1.4 三/五法則
- 如果一個類需要一個析構函數(shù)集嵌,我們幾乎可以肯定它也需要一個拷貝構造函數(shù)和一個拷貝賦值運算符
- 如果一個類需要一個拷貝構造函數(shù)萝挤,幾乎可以肯定它也需要一個拷貝賦值運算符。反之亦然 - 如果一個類需要一個拷貝賦值運算符根欧,幾乎可以肯定它也需要一個拷貝構造函數(shù)怜珍。然而,無論是需要拷貝構造函數(shù)還是需要拷貝賦值運算符都不必然意味著也需要析構函數(shù)
13.1.5 使用 =default
- 我們可以通過將拷貝控制成員定義為 =default 來顯式地要求編譯器生成合成的版本凤粗。合成的版本就是默認版本
13.1.5 阻止拷貝
- 大多數(shù)類應該定義默認構造函數(shù)酥泛、拷貝構造函數(shù)和拷貝賦值運算符,無論是隱式地還是顯式地
- 雖然大多數(shù)類應該定義拷貝構造函數(shù)和拷貝賦值運算符嫌拣,但對某些類來說柔袁,這些操作沒有合理的意義。在此情況下异逐,定義類時必須采用某種機制阻止拷貝或賦值
定義刪除的函數(shù)
- 在新標準下瘦馍,我們可以將拷貝構造函數(shù)和拷貝賦值運算符定義為刪除的函數(shù)來阻止拷貝。
- 刪除的函數(shù)是這樣一種函數(shù): 我們雖然聲明了它們应役,但不能以任何方式使用它們情组。
- 在函數(shù)的參數(shù)列表后面加上 =delete 來指出我們希望將它定義為刪除的。=delete 通知編譯器(以及我們代碼的讀者)箩祥,我們不希望定義這些成員
struct NoCopy{
NoCopy () = default; // 使用合成的默認構造函數(shù)
NoCopy (const NoCopy&) = delete; // 阻止拷貝
NoCopy &operator=(const NoCopy&) = delete; // 阻止賦值
~NoCopy() = default; // 使用合成的析構函數(shù)
};
析構函數(shù)不能是刪除的函數(shù)
- 對于析構函數(shù)已刪除的類型院崇,不能定義該類型的變量或釋放指向該類型動態(tài)分配對象的指針
struct NoDtor{
NoDtor () = default; // 使用默認構造函數(shù)
~NoDtor() = delete; // 我們不能銷毀 NoDtor 類型的對象
}
NoDtor nd; // 錯誤:NoDtor 的析構函數(shù)是刪除的
NoDtor *p = new NoDtor(); // 正確: 但我們不能 delete p
delete p; // 錯誤: NoDtor 的析構函數(shù)是刪除的
合成的拷貝控制成員可能是刪除的
- 本質上,當不可能拷貝袍祖、賦值或銷毀類的成員時底瓣,類的合成拷貝控制成員就被定義為刪除的
private 拷貝控制
- 在新標準發(fā)布之前,類是通過將其拷貝構造函數(shù)和拷貝賦值運算符聲明為 private 來阻止拷貝
class PrivateCopy{
// 無訪問說明符蕉陋;接下來的成員默認是 private
// 拷貝控制成員是 private 的捐凭,因此普通用戶代碼無法訪問
PrivateCopy(const PrivateCopy&);
PrivateCopy &operator=(const PrivateCopy&);
// 其他成員
public:
PrivateCopy() = default; // 使用合成的默認構造函數(shù)
~PrivateCopy(); // 用戶可以定義此類型的對象,但無法使用它們
};
- 由于析構函數(shù)是 public 的凳鬓,用戶可以定義 PrivateCopy 類型的對象茁肠。但是,由于拷貝構造函數(shù)和拷貝賦值運算符是 private 的缩举,用戶代碼將不能拷貝這個類型的對象
建議:希望阻止拷貝的類應該使用 =delete 來定義它們自己的拷貝構造函數(shù)和拷貝賦值運算符垦梆,而不應該將它們聲明為 private 的
13.2 拷貝控制和資源管理
- 類的行為像一個值,意味著它應該也有自己的狀態(tài)仅孩。當我們拷貝一個像值的對象時托猩,副本和原對象是完全獨立的。改變副本不會對原對象有任何影響辽慕,反之亦然
- 行為像指針的類則共享狀態(tài)京腥。當我們拷貝一個這種類的對象時,副本和原對象使用相同的底層數(shù)據(jù)溅蛉。改變副本也會改變原對象公浪,反之亦然
13.2.1 行為像值的類 重點
- C++ Primer 453頁他宛,這一節(jié)太完美了,直接去看書吧因悲,都是重點
- 講的是深拷貝
13.2.2 定義行為像指針的類 重點
- C++ Primer 455頁堕汞,這一節(jié)太完美了,直接去看書吧晃琳,都是重點
- 可以理解為 shared_ptr 的底層實現(xiàn)讯检。(騰訊音樂面試就問到了,可惜當時太菜)
13.3 交換操作
- C++ Primer 457頁卫旱,這一節(jié)太完美了人灼,直接去看書吧,都是重點
- 簡而言之顾翼,直接交換兩個對象的話投放,涉及到深拷貝 (重新分配一塊空間,將將新內容放到這塊空間适贸,原空間內容釋放) 這一系列的操作灸芳,很是沒必要。這節(jié)講的是直接交換指向兩塊空間的指針拜姿。
#include<iostream>
using namespace std;
class AA{
// 友元, 以便訪問 AA 的 private 數(shù)據(jù)成員
friend void swap(AA &l, AA &r);
public:
// 構造函數(shù)
AA(int bb):aa(bb) {
std::cout << aa << " 構造函數(shù)" << endl;
}
// 拷貝構造函數(shù)
AA(const AA &temp){
std::cout << temp.aa << " 拷貝構造函數(shù)" << endl;
this->aa = temp.aa;
}
// 拷貝賦值運算符
AA& operator=(const AA &temp){
std::cout << " 拷貝賦值運算符" << endl;
this->aa = temp.aa;
return *this;
}
~AA(){
std::cout << aa << " 析構函數(shù)" << endl;
}
private:
int aa;
};
// 內聯(lián)函數(shù)
inline void swap(AA &l, AA &r){
std::cout << "交換之前, l.aa = " << l.aa << ", r.aa = " << r.aa << endl;
std::swap(l.aa, r.aa);
std::cout << "交換完成, l.aa = " << l.aa << ", r.aa = " << r.aa << endl;
}
int main(){
AA ff(11); // 調用構造函數(shù)
AA gg = ff; // 調用拷貝構造函數(shù)
AA hh(ff); // 調用拷貝構造函數(shù)
AA kk(77); // 調用構造函數(shù)
kk = ff; // 調用拷貝賦值運算符
AA a(11111);
AA b(22222);
swap(a, b);
}
11 構造函數(shù)
11 拷貝構造函數(shù)
11 拷貝構造函數(shù)
77 構造函數(shù)
拷貝賦值運算符
11111 構造函數(shù)
22222 構造函數(shù)
交換之前, l.aa = 11111, r.aa = 22222
交換完成, l.aa = 22222, r.aa = 11111
11111 析構函數(shù)
22222 析構函數(shù)
11 析構函數(shù)
11 析構函數(shù)
11 析構函數(shù)
11 析構函數(shù)
13.4 拷貝控制示例
13.5 動態(tài)內存管理類
- 實現(xiàn)標準庫 vector 的一個簡化版本蕊肥。功能是不使用模板來實現(xiàn)
- 類在運行時分配可變大小的內存空間
13.6 對象移動
- 新標準的一個最主要特性是可以移動而非拷貝對象的能力谒获。很多情況下都會發(fā)生對象的拷貝。在其中某些情況下壁却,對象拷貝后就立即被銷毀了批狱。在這種情況下,移動而非拷貝對象會大幅度提升性能
- 在上一節(jié)中看到展东,我們的 StrVec 類是這種不必要的拷貝的一個很好的例子赔硫。在重新分配內存的過程中,從舊內存將元素拷貝到新內存是不必要的琅锻,更好的方式是移動元素
13.6.1 右值引用
13.6.2 移動構造函數(shù)和移動賦值運算符
13.6.3 對象移動