拷貝控制
- 當(dāng)定義一個類時讲衫,我們顯式或隱式的指定在此類型的
對象拷貝寒随、移動、賦值和銷毀
時做什么,一個類通過定義五種特殊的成員函數(shù)來控制這些操作
- [x] `拷貝構(gòu)造函數(shù)`
- [x] `拷貝賦值運(yùn)算符`
- [x] `移動構(gòu)造函數(shù)`
- [x] `移動賦值運(yùn)算符`
- [x] `析構(gòu)函數(shù)`
- [x] 拷貝和移動構(gòu)造函數(shù)定義了當(dāng)用同類型的另一個對象初始化本對象時做什么
- [x] 拷貝和移動賦值運(yùn)算符定義了將一個對象賦予同類型的另一個對象時做什么
- [x] 析構(gòu)函數(shù)定義了當(dāng)此類型對象銷毀時做什么
- [x] 上述這些操作統(tǒng)稱為`拷貝控制操作`
- 如果一個類沒有定義上述這些拷貝控制成員顽悼,編譯器會自動為它定義缺失的操作,但對一些類來說几迄,依賴這些操作的默認(rèn)定義會導(dǎo)致災(zāi)難
拷貝蔚龙、賦值與銷毀
拷貝構(gòu)造函數(shù)
- 如果一個構(gòu)造函數(shù)的第一個參數(shù)是自身類類型的引用,且任何額外參數(shù)都有默認(rèn)值映胁,則此構(gòu)造函數(shù)為拷貝構(gòu)造函數(shù)
class Foo{
public:
Foo(); //默認(rèn)構(gòu)造函數(shù)
Foo(const Foo&);//拷貝構(gòu)造函數(shù)
//......
}
- 拷貝構(gòu)造函數(shù)的第一個參數(shù)必須是一個
引用類型
- 當(dāng)我們使用
拷貝初始化
時木羹,我們要求編譯器將右側(cè)運(yùn)算對象拷貝到正在創(chuàng)建的對象中,如果需要的話還要進(jìn)行類型轉(zhuǎn)換- 拷貝初始化是依靠拷貝構(gòu)造函數(shù)或移動構(gòu)造函數(shù)完成的
- 拷貝初始化在下列情況會發(fā)生
- [x] 用`=`定義變量時發(fā)生
- [x] 將一個對象作為實(shí)參傳遞給一個非引用類型的形參
- [x] 從一個返回類型為非引用類型的函數(shù)返回一個對象
- [x] 用或括號列表初始化一個數(shù)組中的元素或一個聚合類中的成員
- 拷貝構(gòu)造函數(shù)自己的參數(shù)必須類型
拷貝賦值運(yùn)算符
- 與類控制其對象如何初始化一樣解孙,類也可以控制器對象如何賦值
Sales_data trans,accum;
trans = accum; //使用Sales_data的拷貝賦值運(yùn)算符
- 與拷貝構(gòu)造函數(shù)一樣坑填,如果類未定義自己的拷貝賦值運(yùn)算符抛人,編譯器會為它合成一個
- 如果一個運(yùn)算符是一個成員函數(shù),則其左側(cè)的運(yùn)算對象就綁定到隱式的this指針參數(shù)
- 賦值運(yùn)算符通常應(yīng)該返回一個指向其左側(cè)運(yùn)算對象的引用
- 標(biāo)準(zhǔn)庫通常要求保存在容器中的類型具有賦值運(yùn)算符
析構(gòu)函數(shù)
- 析構(gòu)函數(shù)執(zhí)行與構(gòu)造函數(shù)相反的操作
- [x] 構(gòu)造函數(shù)初始化對象的非static數(shù)據(jù)成員脐瑰,還可能做一些其他工作
- [x] 析構(gòu)函數(shù)釋放對象使用的資源妖枚,并銷毀對象的非static數(shù)據(jù)成員
- 析構(gòu)函數(shù)沒有返回值,也不接受參數(shù)
- 由于析構(gòu)函數(shù)不接受參數(shù)蚪黑,因此它不能被重載盅惜,對于一個給定類,只會有唯一一個析構(gòu)函數(shù)
- 在一個構(gòu)造函數(shù)中忌穿,成員的初始化是在函數(shù)體執(zhí)行之前完成的抒寂,且按照它們在類中出現(xiàn)的順序進(jìn)行初始化
- 在一個析構(gòu)函數(shù)中,首先執(zhí)行函數(shù)體掠剑,然后銷毀成員屈芜,成員按初始化的逆序進(jìn)行銷毀
隱式銷毀一個內(nèi)置指針類型的成員不會delete它所指向的對象
- 與普通指針不同,智能指針是類類型朴译,所以具有析構(gòu)函數(shù)井佑,因此,與普通指針不同眠寿,智能指針成員在析構(gòu)階段會自動銷毀
- 什么時候會調(diào)用析構(gòu)函數(shù)(
無論何時一個對象被銷毀躬翁,都會自動調(diào)用其析構(gòu)函數(shù)
)
- [x] 變量離開其作用域時被銷毀
- [x] 當(dāng)一個對象被銷毀時,其成員被銷毀
- [x] 容器(無論是標(biāo)準(zhǔn)庫容器還是數(shù)組)被銷毀時盯拱,其元素被銷毀
- [x] 對于動態(tài)分配的對象盒发,當(dāng)對指向它的指針應(yīng)用delete運(yùn)算符時被銷毀
- [x] 對于臨時對象,當(dāng)創(chuàng)建它的完整表達(dá)式結(jié)束時銷毀
- 析構(gòu)函數(shù)自動運(yùn)行狡逢,所以我們的程序可以按需要分配資源宁舰,無需擔(dān)心何時釋放這些資源
- 當(dāng)指向一個對象的引用或指針離開作用域時,析構(gòu)函數(shù)不會執(zhí)行
- 當(dāng)一個類未定義自己的析構(gòu)函數(shù)時奢浑,編譯器會為它定義一個合成析構(gòu)函數(shù)蛮艰,類似拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符
析構(gòu)函數(shù)體自身并不直接銷毀成員,成員是在析構(gòu)函數(shù)體之后隱含的析構(gòu)階段被銷毀的
雀彼,在整個對象銷毀過程中壤蚜,析構(gòu)函數(shù)整體是作為成員銷毀步驟之外的另一部分而進(jìn)行的
三/五法則
- 如果一個類需要一個析構(gòu)函數(shù),那么它幾乎可以肯定也需要一個拷貝構(gòu)造函數(shù)和一個拷貝賦值運(yùn)算符
- 如果一個類需要一個拷貝構(gòu)造函數(shù)详羡,幾乎可以肯定它也需要一個拷貝賦值運(yùn)算符仍律,反之亦然
- 無論是需要拷貝構(gòu)造函數(shù)還是需要拷貝賦值運(yùn)算符都不洗染意味著需要析構(gòu)函數(shù)
使用=default
- 我們可以通過拷貝控制成員定義=default來顯式的要求編譯器生成合成版本
class Sales_data{
public:
//拷貝控制成員;使用default
Sales_data() = default;
Sales_data(const Sales_data&) = default;
Sales_data operator=(const Sales_data &);
~Sales_data()=default;
//其余成員定義如前
}
- 我們只能對具有合成版本的成員函數(shù)使用=default(即实柠,默認(rèn)構(gòu)造函數(shù)或拷貝控制成員)
阻止拷貝
- 大多數(shù)類應(yīng)該定義默認(rèn)構(gòu)造函數(shù)水泉、拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符,無論是隱式的還是顯式的
- 當(dāng)一個類明確不允許拷貝賦值操作時,必須使用=delete將拷貝構(gòu)造函數(shù)與拷貝賦值運(yùn)算符定義為刪除的,顯式的阻止該函數(shù)被編譯器默認(rèn)生成草则,如iostream類
- 通過=delete可以將拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符定義為
刪除的函數(shù)
來阻止拷貝钢拧,刪除的函數(shù)是這樣一種函數(shù):我們雖然聲明了它們,但不能以任何方式使用炕横,在函數(shù)的參數(shù)列表后面加上=delete來指出我們希望將它定義為刪除的
struct NoCopy{
NoCopy() = default; //使用合成的默認(rèn)構(gòu)造函數(shù)
NoCopy(const NoCopy&) = delete; //阻止拷貝
NoCopy & operator=(const NoCopy&) = delete; //阻止賦值
};
- =delete通知編譯器源内,我們不希望定義這些成員
- =delete必須出現(xiàn)在函數(shù)第一次聲明的時候
- 我們可以對任何函數(shù)指定=delete,但只能對編譯器可以合成默認(rèn)函數(shù)的默認(rèn)構(gòu)造函數(shù)或拷貝控制成員使用=default
- 雖然刪除函數(shù)的主要用途是禁止拷貝控制成員份殿,但當(dāng)我們希望引導(dǎo)函數(shù)匹配過程時膜钓,刪除函數(shù)有時也是有用的
需要注意的是,我們不能刪除析構(gòu)函數(shù)
卿嘲,如果析構(gòu)函數(shù)被刪除颂斜,我們不能定義該類的變量或臨時對象(編譯器不允許)- 對于刪除了析構(gòu)函數(shù)的類型,雖然我們不能定義這種類型的變量或成員拾枣,但可以動態(tài)分配這種類型的對象沃疮,但是不能釋放這些對象
struct NoDtor{
NoDtor() = default;
~NoDtor() = delete;
};
NoDtor nd; //錯誤,析構(gòu)函數(shù)刪除不能定義變量
NoDtor * p = new NoDtor(); //正確梅肤,但不能delete
delete p; //錯誤司蔬,析構(gòu)函數(shù)是刪除的
- 對于析構(gòu)函數(shù)已刪除出的類型,不能定義該類型的變量或釋放指向該類型動態(tài)分配對象的指針
- 在新標(biāo)準(zhǔn)發(fā)布之前(C++11)姨蝴,類是通過將其拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符聲明為
private
的來阻止拷貝- 聲明但不定義一個成員函數(shù)是合法的
- 希望阻止拷貝的類應(yīng)用應(yīng)該使用=delete來定義它們自己的拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符俊啼,而不應(yīng)該將它們聲明為private
拷貝控制和資源管理
- 一旦一個類需要析構(gòu)函數(shù),那么幾乎肯定它也需要一個拷貝構(gòu)造函數(shù)和一個拷貝控制運(yùn)算符
行為像值的類
- 當(dāng)編寫賦值運(yùn)算符時左医,以下兩點(diǎn)需要記住
- [x] 如果一個對象賦予它自身吨些,賦值運(yùn)算符必須能正確工作
- [x] 大多數(shù)賦值運(yùn)算符組合了析構(gòu)函數(shù)和拷貝構(gòu)造函數(shù)的工作
- 當(dāng)你編寫一個賦值運(yùn)算符,一個好的模式是先將右側(cè)的運(yùn)算對象拷貝到一個局部臨時對象中炒辉,當(dāng)拷貝完成后,銷毀左側(cè)運(yùn)算對象的現(xiàn)有成員就是安全的了泉手,一旦左側(cè)運(yùn)算對象的資源被銷毀黔寇,就只剩下將數(shù)據(jù)從臨時對象拷貝到左側(cè)運(yùn)算對象的成員中了
- 對于一個賦值運(yùn)算符來說,正確工作是非常重要的斩萌,即使是將一個對象賦予它自身缝裤,也要能正確工作,一個好的方法是在銷毀左側(cè)運(yùn)算對象資源之前拷貝右側(cè)運(yùn)算對象
定義行為像指針的類
- 對于行為類似指針的類颊郎,我們需要為其定義拷貝構(gòu)造函數(shù)和拷貝賦值運(yùn)算符憋飞,來拷貝指針成員本身而不是它指向的string,我們的類仍需自己的析構(gòu)函數(shù)來釋放接受string參數(shù)的構(gòu)造函數(shù)分配的內(nèi)存
- 一個令類展現(xiàn)類似指針的行為的最好方法是使用shared_ptr來管理類中的資源
- 當(dāng)我們希望直接管理資源的時候姆吭,就要使用
引用計數(shù)
- 引用計數(shù)的工作方式如下:
- [x] 除了初始化對象外榛做,每個構(gòu)造函數(shù)(拷貝構(gòu)造函數(shù)除外)還有創(chuàng)建一個引用計數(shù),用來記錄有多少對象與正在創(chuàng)建的對象共享狀態(tài),當(dāng)我們創(chuàng)建一個對象時检眯,只有一個對象共享狀態(tài)厘擂,因此計數(shù)器初始化為1
- [x] 拷貝構(gòu)造函數(shù)不分配新的計數(shù)器,而是拷貝給定的數(shù)據(jù)成員锰瘸,包括計數(shù)器刽严,拷貝構(gòu)造函數(shù)遞增共享的計數(shù)器,指出給定對象是的狀態(tài)又被一個新用戶所共享
- [x] 析構(gòu)函數(shù)遞減計數(shù)器避凝,指出共享狀態(tài)的用戶少了一個舞萄,如果計數(shù)器變?yōu)?,則析構(gòu)函數(shù)釋放狀態(tài)
- [x] 拷貝賦值運(yùn)算符遞增右側(cè)運(yùn)算對象的計數(shù)器管削,遞減左側(cè)運(yùn)算對象的計數(shù)器倒脓,如果左側(cè)運(yùn)算對象的計數(shù)器變?yōu)?,意味著它的共享狀態(tài)沒有用戶了佩谣,拷貝賦值運(yùn)算符就必須銷毀狀態(tài)
- 引用計數(shù)器保存在動態(tài)內(nèi)存中把还,當(dāng)創(chuàng)建一個對象時,我們也分配一個新的計數(shù)器茸俭,當(dāng)拷貝或賦值對象時吊履,我們拷貝指向計數(shù)器的指針,使用這種方法调鬓,副本和原對象都會指向相同的計數(shù)器
交換操作
- 除了定義拷貝控制成員艇炎,管理資源的類通常還定義一個名為swap的函數(shù),對于哪些與重排元素順序的算法一起使用的類腾窝,定義swap是非常重要的缀踪,這類算法在需要交換兩個元素時會調(diào)用swap
- 如果一個類定義了自己的swap,那么算法將會使用類自定義版本虹脯,否則驴娃,算法將使用標(biāo)準(zhǔn)庫定義的swap
- 如下為swap實(shí)現(xiàn)的兩種方式
/*交換值空間*/
HasPtr temp = v1; //創(chuàng)建v1的值的一個臨時副本
v1 = v2; //將v2的值賦予v1
v2 = temp; //將保存的v1的值賦予v2
/*直接交換指針*/
string * temp = v1.ps; //為v1.ps中的指針創(chuàng)建一個副本
v1.ps = v2.ps; //將v2.ps中的指針賦予v1.ps
v2.ps = temp; //將保存的v1.ps中原來的指針賦予v2.ps
- swap的經(jīng)典實(shí)現(xiàn)如下
class HasPtr{
friend void swap(HasPtr & , HasPtr &);
//其余成員定義
};
inline void swap(HasPtr & 1hs, HasPtr & rhs){
using std::swap;
swap(1hs.ps,rhs.ps);
swap(1hs.i,rhs,i);
}
- 與拷貝控制成員不同,swap并不是必要的循集,但對于分配了資源的類唇敞,定義swap可能是一種很重要的優(yōu)化手段
swap函數(shù)應(yīng)該調(diào)用swap,而不是std::swap
- 如果一個類的成員有自己類型特定的swap函數(shù)咒彤,調(diào)用std::swap就是錯誤的疆柔,反之則正確
動態(tài)內(nèi)存管理類
- 以下將實(shí)現(xiàn)標(biāo)準(zhǔn)庫vector類的一個簡化版本,非模板且僅適用與string镶柱,將其命名為StrVec
- 每個StrVec有三個指針成員指向其元素所使用的內(nèi)存:
- [X] elements,指向分配的內(nèi)存中的首元素
- [x] first_free,指向最后一個實(shí)際元素之和的位置
- [x] cap,指向分配的內(nèi)存末尾之后的位置
- 上述指針的含義如下圖
![Capture.PNG-17.2kB][1]- StrVec定義如下
class StrVec{
public:
StrVec():
elements(nullptr),first_ptr(nullptr),cap(nullptr){}
StrVec(const StrVec &); //拷貝構(gòu)造函數(shù)
StrVec & operator=(const StrVec &); //拷貝賦值運(yùn)算符
~StrVec(); //析構(gòu)函數(shù)
void push_back(const std::string &); //拷貝元素
size_t size() const {return first_free - elements;}
size_t capacity() const {return cap - elements;}
std::string * begin() const {return elements;}
std::string * end() const {return first_free;}
//...
private:
static std::allocator<std::string> alloc; //分配元素
/*被添加元素的函數(shù)所使用*/
void chk_n_alloc(){
if(size() == capacity())
reallocate();
}
/*工具函數(shù)旷档,被拷貝構(gòu)造函數(shù),賦值運(yùn)算符和析構(gòu)函數(shù)所使用*/
std::pair<std::string* , std::string*>alloc_n_copy(const std::string* , const std::string*);
void free(); //銷毀元素并釋放內(nèi)存
void reallocate(); //獲得更多內(nèi)存并拷貝已有元素
std::string * elements; //指向數(shù)組首元素的指針
std::string * first_ptr; //指向數(shù)組第一個空閑元素的指針
std::string * cap; //指向數(shù)組尾后位置的指針
};
- 上述代碼解釋
- [x] 默認(rèn)構(gòu)造函數(shù)(隱式的)默認(rèn)初始化alloc并(顯式的)將指針初始化為nullptr歇拆,表明沒有元素
- [x] size成員返回當(dāng)前真正在使用的元素的數(shù)目鞋屈,等于first_free --- elements
- [x] capacity成員返回StrVec可以保存的元素的數(shù)量范咨,等價于cap --- elements
- [x] 當(dāng)沒有空間容納新元素,即cap==first_free時谐区,chk_n_alloc會為StrVec重新分配內(nèi)存
- [x] begin和end成員分別返回指向首元素(即elements)和最后一個構(gòu)造的元素之和的位置(即first_free)的指針
- 使用alloctor的時候湖蜕,必須記住內(nèi)存是
未構(gòu)造的
,為了使用此原始內(nèi)存宋列,必須調(diào)用construct昭抒,在內(nèi)存中構(gòu)造一個對象,alloctor的使用如下
allocator<int> alloc; //構(gòu)建int分配內(nèi)存的alloctor
int * temp = alloc.allocate(10); //分配10個int對象的空間炼杖,而不調(diào)用構(gòu)造函數(shù)
alloc.construct(temp); //在第一個對象的內(nèi)存上通過缺省構(gòu)造函數(shù)構(gòu)建對象
alloc.construct(temp+1,6); //在第二個對象的內(nèi)存上通過有參構(gòu)造函數(shù)構(gòu)建對象
alloc.destroy(temp+1); //調(diào)用第二個對象的析構(gòu)函數(shù)(不釋放內(nèi)存)
alloc.destroy(temp); //調(diào)用第一個對象的析構(gòu)函數(shù)(不釋放內(nèi)存)
alloc.deallocate(temp,10); //釋放內(nèi)存
對象移動
- 標(biāo)準(zhǔn)容器庫灭返,string和shared_ptr類既支持移動也支持拷貝,IO類和unique_ptr類可以移動但不可以拷貝
右值引用
- 右值引用就是必須綁定到右值的引用坤邪,我們通過&&而不是&來獲得右值的引用
- 右值引用只能綁定到一個將要銷毀的對象
- 常規(guī)引用被稱為左值引用
- 左值引用不能將其綁定到要求轉(zhuǎn)換的表達(dá)式/字面常量或是返回右值的表達(dá)式
- 右值引用有著完全相反的特性熙含,我們可以將一個右值引用綁定到這類表達(dá)式上,但不能將一個右值引用直接綁定到一個左值上
int i = 42;
int & r = i; //正確艇纺,r引用i
int && rr = i; //錯誤怎静,不能將一個右值引用綁定到一個左值上
int & r2 = i * 42; //錯誤,i*42是一個右值
const int & r3 = i * 42; //正確黔衡,常量引用可以綁定右值
int && rr2 = i * 42; //正確蚓聘,將右值引用rr2綁定在乘法結(jié)果上
- 返回左值引用的函數(shù),連同賦值/下標(biāo)/解引用和前置遞增/遞減運(yùn)算符盟劫,都是返回左值表達(dá)式的例子夜牡,我們可以將一個左值引用綁定到這類表達(dá)式的結(jié)果上
- 返回非音樂類型的函數(shù),連同算數(shù)/關(guān)系/位以及后置遞增/遞減運(yùn)算符侣签,都生成右值塘装,我們不能將一個左值引用綁定到這類表達(dá)式上,但我們可以將一個const的左值引用或者一個右值引用綁定到這類表達(dá)式上
- 左值有持久的狀態(tài)影所,而右值要么是字面常量有蹦肴,要么是在表達(dá)式求值過程中創(chuàng)建的臨時對象
- 由右值引用只能綁定到臨時對象可知
- [x] 所引用的對象將要被銷毀
- [x] 該對象沒有其他用戶
- 使用右值引用的代碼可以自由的接管所引用的對象的資源
- 右值引用指向?qū)⒁讳N毀的對象,因此猴娩,我們可以從綁定到右值的對象竊取狀態(tài)
- 變量是左值冗尤,因此我們不能將一個右值引用直接綁定到一個變量上,即使這個變量是右值引用類型也不行
- 我們可以銷毀一個移動后源對象胀溺,也可以賦予它新值,但不能使用一個移后源對象的值
- 使用move的代碼應(yīng)該使用std::move而不是move皆看,這樣可以避免潛在的名字沖突