首先慢洋,本章很長(zhǎng)塘雳,也較難理解陆盘,建議讀者有大段連續(xù)的時(shí)間看這個(gè)。败明。隘马。
3.3.1 指針成員與拷貝構(gòu)造
關(guān)于拷貝構(gòu)造函數(shù)的調(diào)用時(shí)間,可以看這篇文章妻顶。
如果類(lèi)中包含了指針酸员,需要小心處理蜒车,下面是一段有問(wèn)題的代碼
class C {
public:
C():i(new int(0)){
cout << "none argument constructor called" << endl;
}
~C(){
cout << "destructor called" << endl;
delete i;
}
int* i;
};
int main(){
C c1;
C c2 = c1;
cout << *c1.i << endl;
cout << *c2.i << endl;
return 0;
}
XCode代碼執(zhí)行輸出
none argument constructor called
0
0
destructor called
destructor called
CppTest(50956,0x1000ad5c0) malloc: *** error for object 0x10070c510: pointer being freed was not allocated
CppTest(50956,0x1000ad5c0) malloc: *** set a breakpoint in malloc_error_break to debug
原因是編譯期會(huì)默認(rèn)為類(lèi)創(chuàng)建拷貝構(gòu)造函數(shù),而默認(rèn)的拷貝構(gòu)造函數(shù)只是簡(jiǎn)單的賦值幔嗦,對(duì)類(lèi)C酿愧,系統(tǒng)默認(rèn)生成的拷貝構(gòu)造函數(shù)如
C(const C& c):i(c.i){
}
導(dǎo)致c1和c2的i值一樣,即指向同一片地址邀泉,當(dāng)c1析構(gòu)之后嬉挡,c2.i就成為了一個(gè)“懸掛指針”(dangling pointer),不再指向有效的內(nèi)存了汇恤,如果對(duì)懸掛指針再次進(jìn)行delete就會(huì)出現(xiàn)嚴(yán)重的錯(cuò)誤庞钢。
以上系統(tǒng)生成的默認(rèn)拷貝構(gòu)造函數(shù)做的是淺拷貝(shallow copy),為了解決這個(gè)問(wèn)題因谎,通常是用戶(hù)自定義拷貝構(gòu)造函數(shù)實(shí)現(xiàn)深拷貝(deep copy)基括,修正如下
class C {
public:
C():i(new int(0)){
cout << "none argument constructor called" << endl;
}
//增加此拷貝構(gòu)造函數(shù),根據(jù)傳入的c财岔,new一個(gè)新的int給i變量
C(const C& c) :i(new int(*c.i)){
}
~C(){
cout << "destructor called" << endl;
delete i;
}
int* i;
};
執(zhí)行代碼后如下
none argument constructor called
0
0
destructor called
destructor called
Program ended with exit code: 0
3.3.2 移動(dòng)語(yǔ)義
拷貝函數(shù)中為指針成員分配新的內(nèi)存再進(jìn)行內(nèi)容拷貝的方法在C++中幾乎被視為不可違背的风皿,不過(guò)有些時(shí)候卻是不必要的。如下代碼:
//這是一個(gè)成員包含指針的類(lèi)
class HasPtrMem {
public:
HasPtrMem() : d(new int(0)) {
cout << "Construct:" << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem& h) {
cout << "Copy construct:" << ++n_cptr << endl;
}
~HasPtrMem() {
cout << "Destruct:" << ++n_dstr << endl;
}
private:
int* d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
HasPtrMem GetTemp() {
return HasPtrMem();//①
}
int main(){
HasPtrMem m = GetTemp();//②
return 0;
}
這里我沒(méi)用Xcode編譯運(yùn)行使鹅,因?yàn)樵贐uild Setting里增加-fno-elide-constructors編譯器依然還是優(yōu)化了揪阶,所以根據(jù)教材用命令行執(zhí)行
g++ -std=c++11 main.cpp -fno-elide-constructors
會(huì)在cpp文件下生成一個(gè)a.out文件,在命令行執(zhí)行./a.out
輸出
Construct:1
Copy construct:1
Destruct:1
Copy construct:2
Destruct:2
Destruct:3
構(gòu)造函數(shù)被調(diào)用1次患朱,是在①處鲁僚,第一次調(diào)用拷貝構(gòu)造函數(shù)是在GetTemp return的時(shí)候,將①生成的變量拷貝構(gòu)造出一個(gè)臨時(shí)值裁厅,來(lái)當(dāng)做GetTemp的返回冰沙,第二次拷貝構(gòu)造函數(shù)是在②處。同時(shí)就有了于此對(duì)應(yīng)的三次析構(gòu)函數(shù)的調(diào)用执虹。例子里用的是一個(gè)int類(lèi)型的指針拓挥,而如果該指針指向的是非常大的堆內(nèi)存數(shù)據(jù)的話(huà),那沒(méi)拷貝過(guò)程就會(huì)非常耗時(shí)袋励,而且由于整個(gè)行為是透明且正確的侥啤,分析問(wèn)題時(shí)也不易察覺(jué)。
在C++中茬故,我們可以通過(guò)移動(dòng)構(gòu)造函數(shù)解決此問(wèn)題盖灸,修改代碼如下:
//這是一個(gè)成員包含指針的類(lèi)
class HasPtrMem {
public:
HasPtrMem() : d(new int(0)) {
cout << "Construct:" << ++n_cstr << endl;
}
HasPtrMem(const HasPtrMem& h) {
cout << "Copy construct:" << ++n_cptr << endl;
}
HasPtrMem(HasPtrMem&& h):d(h.d) {
h.d = nullptr; //③注意對(duì)之前的h賦空指針
cout << "Move construct:" << ++n_mvtr << endl;
}
~HasPtrMem() {
cout << "Destruct:" << ++n_dstr << endl;
}
private:
int* d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
int HasPtrMem::n_cstr = 0;
int HasPtrMem::n_dstr = 0;
int HasPtrMem::n_cptr = 0;
int HasPtrMem::n_mvtr = 0;
HasPtrMem GetTemp() {
return HasPtrMem();
}
int main(){
HasPtrMem m = GetTemp();
return 0;
}
輸出
Construct:1
Move construct:1
Destruct:1
Move construct:2
Destruct:2
Destruct:3
這里通過(guò)指針賦值的方式,將d的內(nèi)存直接偷了過(guò)來(lái)磺芭,避免了拷貝構(gòu)造函數(shù)的調(diào)用赁炎。注意③,這里需要對(duì)原來(lái)的d進(jìn)行賦空值钾腺,因?yàn)樵谝苿?dòng)構(gòu)造函數(shù)完成之后徙垫,臨時(shí)對(duì)象會(huì)立即被析構(gòu)讥裤,如果不改變d,那臨時(shí)對(duì)象被析構(gòu)時(shí)姻报,因?yàn)橥祦?lái)的d和原本的d指向同一塊內(nèi)存己英,會(huì)被釋放,成為懸掛指針吴旋,會(huì)造成錯(cuò)誤剧辐。
為什么不用函數(shù)參數(shù)里帶個(gè)指針或者引用當(dāng)返回結(jié)果呢?不是性能的問(wèn)題邮府,而是代碼編寫(xiě)效率及可讀性不好荧关,如:
string *a;
int c = 1
int &b = c;
Calculate(GetTemp(),b);//最后一個(gè)參數(shù)用于返回結(jié)果
最后說(shuō)明一下移動(dòng)構(gòu)造函數(shù)被調(diào)用的時(shí)機(jī):一旦用到的是臨時(shí)變量,那么移動(dòng)語(yǔ)義就可以得到執(zhí)行褂傀。下一節(jié)講下C++的值是如何分類(lèi)的忍啤。未完待續(xù),后面還有4節(jié)仙辟。同波。。
3.3.3 左值叠国、右值與右值引用
關(guān)于左值(lvalue)和右值(rvalue)的判別方法:
- 在賦值表達(dá)式中未檩,出現(xiàn)在等號(hào)左邊的是“左值”,等號(hào)右邊的是“右值”粟焊,如
a = b + c;
中冤狡,a是左值,而b+c是右值项棠; - 可以取地址的悲雳、有名字的是左值,反之是右值香追,對(duì)于
a = b + c;
合瓢,&a是允許的操作,&(b+c)是不允許的操作透典,所以a是左值晴楔,b+c是右值。
而在C++11中右值是由兩個(gè)概念構(gòu)成的峭咒,一個(gè)是將亡值(xvalue, eXpriring Value)税弃,另個(gè)一個(gè)則是純右值(prvalue, Pure Rvalue)。
其中純右值包括:
- 非引用返回的函數(shù)返回的臨時(shí)變量值
- 運(yùn)算表達(dá)式讹语,如1+3產(chǎn)生的臨時(shí)變量值
- 不跟對(duì)象關(guān)聯(lián)的字面量钙皮,如2蜂科、’c‘顽决、true
- 類(lèi)型轉(zhuǎn)換函數(shù)的返回值
- lamda表達(dá)式
將亡值賊是C++11新增的跟右值引用相關(guān)的表達(dá)式短条,包括:
- 返回右值引用T&&的函數(shù)返回值
- std::move的返回值
- 轉(zhuǎn)換為T(mén)&&的類(lèi)型轉(zhuǎn)換函數(shù)的返回值
而剩余的,可以標(biāo)識(shí)函數(shù)才菠、對(duì)象的值都屬于左值茸时。在C++11的程序中,所有的值必屬于左值赋访、將亡值可都、純右值三者之一。
在C++11中蚓耽,右值引用就是對(duì)一個(gè)右值進(jìn)行引用的類(lèi)型渠牲。由于右值不具有名字,我們也只能通過(guò)引用的方式找到它的存在步悠。通常我們只能是從右值表達(dá)式獲得其引用签杈。比如:
T&& a = ReturnRvalue();①
右值引用和左值引用都是引用類(lèi)型,都必須立即進(jìn)行初始化鼎兽。引用類(lèi)型本身并不擁有綁定對(duì)象的內(nèi)存答姥,只是該對(duì)象的一個(gè)別名。左值引用是具名變量值的別名谚咬,右值引用則是匿名變量的別名鹦付。
在上面①的例子中,ReturnRvalue函數(shù)返回的右值在表達(dá)式語(yǔ)句結(jié)束后择卦,其生命也就終結(jié)了敲长,而通過(guò)右值引用的聲明,該右值又“重獲新生”秉继,其生命期將于右值引用類(lèi)型a的生命期一樣潘明。只要a還“活著”,該右值臨時(shí)量將會(huì)一直“存活”下去秕噪。
所以相比于一下語(yǔ)句:
T b = ReturnRvalue();
①的聲明方式會(huì)少一次對(duì)象的析構(gòu)和一次對(duì)象構(gòu)造钳降。因?yàn)閍是右值引用,直接綁定了ReturnRvalue()返回的臨時(shí)量腌巾,而b是由臨時(shí)值構(gòu)造的遂填,而臨時(shí)量在表達(dá)式結(jié)束后會(huì)析構(gòu)因而會(huì)多一次析構(gòu)和構(gòu)造的開(kāi)銷(xiāo)。
注意澈蝙,能夠聲明右值引用a的前提是ReturnRvalue返回的是一個(gè)右值吓坚。通常右值引用是不能夠綁定到任何左值的,如下代碼會(huì)導(dǎo)致編譯無(wú)法通過(guò):
int c;
int &&d = c;
有的時(shí)候灯荧,我們可能不知道一個(gè)類(lèi)型是否是引用類(lèi)型礁击,以及是左值引用還是右值引用。標(biāo)準(zhǔn)庫(kù)<type_traits>頭文件中提供了3個(gè)類(lèi)模板:is_rvalue_reference、is_lvalue_reference和is_reference哆窿,比如:
cout << is_rvalue_reference<string &&>::value;
3.3.4 std::move 強(qiáng)制轉(zhuǎn)化為右值
C++11中链烈,<utility>中提供了函數(shù)std::move,功能是將一個(gè)左值強(qiáng)制轉(zhuǎn)化為右值引用挚躯,繼而我們可以通過(guò)右值引用使用該值强衡,用于移動(dòng)語(yǔ)義。std::move基本等同于一個(gè)類(lèi)型轉(zhuǎn)換:
static_cast<T&&>(lvalue);
被轉(zhuǎn)化的左值码荔,其生命期并沒(méi)有隨著左右值的轉(zhuǎn)化而改變漩勤。下面是一個(gè)正確使用std::move的例子
class HugeMem {
public:
HugeMem(int size): sz(size>0 ? size: 1) {
c = new int[size];
}
~HugeMem() {
delete [] c;
}
HugeMem(HugeMem&& h) : sz(h.sz), c(h.c) {
h.c = nullptr;
}
int* c;
int sz;
};
class Moveable {
public:
Moveable(): i(new int[3]), h(1024) {}
~Moveable() {
delete [] i;
}
Moveable(Moveable&& m) : i(m.i), h(move(m.h)) { //使用move將m.h轉(zhuǎn)為右值引用,繼而調(diào)用HugeMem的移動(dòng)構(gòu)造函數(shù)
m.i = nullptr;
}
int *i;
HugeMem h;
};
Moveable getTemp() {
Moveable tmp = Moveable();
cout << hex << "Huge Mem from " << __func__ << "@" << tmp.h.c << endl;
return tmp;
}
int main(){
Moveable a(getTemp());//因?yàn)間etTemp()返回的是右值缩搅,所以會(huì)調(diào)用Moveable的移動(dòng)構(gòu)造函數(shù)
cout << hex << "Huge Mem from " << __func__ << "@" << a.h.c << endl;
return 0;
}
輸出
Huge Mem from getTemp@0x104002000
Huge Mem from main@0x104002000
需要注意的是越败,在編寫(xiě)移動(dòng)構(gòu)造函數(shù)的時(shí)候,應(yīng)該總是使用std::move轉(zhuǎn)換擁有形如堆內(nèi)存硼瓣、文件句柄的等資源的成員為右值眉尸,這樣一來(lái),如果成員支持移動(dòng)構(gòu)造的話(huà)巨双,就可以實(shí)現(xiàn)其移動(dòng)語(yǔ)義噪猾,即使成員沒(méi)有移動(dòng)構(gòu)造函數(shù),也會(huì)調(diào)用拷貝構(gòu)造筑累,因?yàn)椴粫?huì)引起大的問(wèn)題袱蜡。
3.3.5 移動(dòng)語(yǔ)義的一些其他問(wèn)題
移動(dòng)語(yǔ)義一定是要改變臨時(shí)變量的值(這里有以為,需要解決慢宗,目前沒(méi)看出哪里一定要改變坪蚁,先這么硬背吧)。如聲明:
Moveable(const Moveale &&);//這個(gè)對(duì)應(yīng)3.3.4的例子镜沽,如果這樣聲明移動(dòng)構(gòu)造函數(shù)會(huì)報(bào)錯(cuò)
而如果是將3.3.4的例子中的Moveable getTemp()
改為const Moveable getTemp()
,再執(zhí)行命令
g++ -std=c++11 main.cpp -fno-elide-constructors
注意上面的改動(dòng)在Xcode中是可以運(yùn)行的敏晤,可以正確調(diào)用到移動(dòng)構(gòu)造函數(shù),但是通過(guò)命令行會(huì)提示
copy constructor is implicitly deleted because 'Moveable' has a user-declared move constructor
可見(jiàn)Moveable a(getTemp());
實(shí)際是要調(diào)用Moveable的拷貝構(gòu)造函數(shù)缅茉。報(bào)錯(cuò)原因顯示聲明了移動(dòng)構(gòu)造函數(shù)嘴脾,編譯器就不會(huì)為類(lèi)生成默認(rèn)的拷貝構(gòu)造函數(shù)了,所以提示沒(méi)有顯示聲明拷貝構(gòu)造函數(shù)蔬墩。
在C++11中译打,拷貝/移動(dòng)改造函數(shù)有以下3個(gè)版本:
- T Object(T&)
- T Object(const T&)
- T Object(T&&)
其中常量左值引用的版本是一個(gè)拷貝構(gòu)造函數(shù)版本,右值引用參數(shù)的是一個(gè)移動(dòng)構(gòu)造函數(shù)版本拇颅。默認(rèn)情況下奏司,編譯器會(huì)為程序員隱式地生成一個(gè)移動(dòng)構(gòu)造函數(shù),但是如果聲明了一自定義的拷貝構(gòu)造函數(shù)樟插、拷貝賦值函數(shù)韵洋、移動(dòng)構(gòu)造函數(shù)竿刁、析構(gòu)函數(shù)中的一個(gè)或者多個(gè),編譯器都不會(huì)再生成默認(rèn)版本搪缨。所以在C++11中食拜,拷貝構(gòu)造函數(shù)、拷貝賦值函數(shù)勉吻、移動(dòng)構(gòu)造函數(shù)和移動(dòng)賦值函數(shù)必須同時(shí)提供,或者同時(shí)不提供旅赢,只聲明其中一種的話(huà)齿桃,類(lèi)都僅能實(shí)現(xiàn)一種語(yǔ)義。
只實(shí)現(xiàn)一種語(yǔ)義在類(lèi)的編寫(xiě)中也是非常常見(jiàn)的煮盼,比如如果只實(shí)現(xiàn)移動(dòng)語(yǔ)義短纵,則表明該類(lèi)型的變量擁有的資源只能被移動(dòng),不能被拷貝僵控,那么這樣的資源必須是唯一的香到,如智能指針、文件流报破。
在<type_traits>里悠就,可以通過(guò)一些輔助的模板類(lèi)來(lái)判斷一個(gè)類(lèi)型是否是可以移動(dòng)的,如:
- is_move_constructible
- is_trivially_move_constructible
- is_nothrow_move_constructible
使用方法都是使用value成員充易,如
cout << is_move_constructible<UnknowTYpe>::value;
有了移動(dòng)語(yǔ)義梗脾,可以實(shí)現(xiàn)高性能的置換函數(shù),如:
template <class T>
void swap(T& a, T& b) {
T tmp(move(a));
a = move(b);
b = move(tmp);
}
如果T是可以移動(dòng)的盹靴,則不會(huì)有資源的釋放和申請(qǐng)炸茧,如果T不可移動(dòng)但是可以拷貝,則和普通聲明一樣了稿静。
要注意的是梭冠,盡量不要編寫(xiě)會(huì)拋出異常的移動(dòng)構(gòu)造函數(shù),因?yàn)橛锌赡芤苿?dòng)沒(méi)完成改备,會(huì)導(dǎo)致一些指針成為懸掛指針控漠,通過(guò)添加noexcept關(guān)鍵字,可以保證移動(dòng)構(gòu)造函數(shù)拋出異常直接終止程序悬钳。
3.3.6 完美轉(zhuǎn)發(fā)
完美轉(zhuǎn)發(fā)(perfect forwarding)润脸,是指在模板函數(shù)中,完全依照模板的參數(shù)類(lèi)型講參數(shù)傳遞給模板中調(diào)用的另外一個(gè)函數(shù)他去,如:
template <typename T>
void IamForwarding(T t) {
IrunCodeActually(t);
}
這是一個(gè)參數(shù)透?jìng)鞯膶?shí)現(xiàn)毙驯,但是因?yàn)槭褂米罨绢?lèi)型轉(zhuǎn)發(fā),會(huì)在傳參的時(shí)候產(chǎn)生一次額外的臨時(shí)對(duì)象拷貝灾测,因?yàn)橹荒苷f(shuō)是轉(zhuǎn)發(fā)爆价,但不完美垦巴。所以通常需要的是一個(gè)引用類(lèi)型餐護(hù)士,不會(huì)有拷貝的開(kāi)銷(xiāo)铭段。其次需要考慮函數(shù)對(duì)類(lèi)型的接受能力骤宣,因?yàn)槟繕?biāo)函數(shù)可能需要既接受左值引用,又接受右值引用序愚,如果轉(zhuǎn)發(fā)函數(shù)只能接受其中的一部分憔披,也不完美。
對(duì)應(yīng)代碼
typedef const A T;
typedef T& TR;
TR& v = 1;
在C++11中引入了一條所謂“引用折疊”的新語(yǔ)言規(guī)則爸吮,規(guī)則如下
TR的類(lèi)型定義 | 聲明v的類(lèi)型 | v的實(shí)際類(lèi)型 |
---|---|---|
T& | TR | A& |
T& | TR& | A& |
T& | TR&& | A& |
T&& | TR | A&& |
T&& | TR& | A& |
T&& | TR&& | A&& |
規(guī)則就是一單定義中出現(xiàn)了左值引用芬膝,引用折疊總是優(yōu)先將其折疊為左值引用。前三行TR定義為T(mén)&形娇,則v世界類(lèi)型為A&锰霜,第五行的v的類(lèi)型為T(mén)R&,則v的實(shí)際類(lèi)型也為A&桐早,其他則為右值引用癣缅。于是我們把轉(zhuǎn)發(fā)函數(shù)改為:
template <typename T>
void IamForwarding(T&& t) {
IrunCodeActually(static_cast<T&&>(t));
}
對(duì)于傳入的左值引用
void IamForwarding(X& && t) {
IrunCodeActually(static_cast<X& &&>(t));
}
折疊后是
void IamForwarding(X& t) {
IrunCodeActually(static_cast<X&>(t));
}
對(duì)于右值引用
void IamForwarding(X&& && t) {
IrunCodeActually(static_cast<X&& &&>(t));
}
折疊后是
void IamForwarding(X&& t) {
IrunCodeActually(static_cast<X&&>(t));
}
此處的static_cast類(lèi)似std::move的作用,將左值轉(zhuǎn)換為右值引用哄酝。不過(guò)在C++11中友存,用于完美轉(zhuǎn)發(fā)的函數(shù)不叫move,叫forward陶衅,所以也可以這么寫(xiě)
void IamForwarding(X&& t) {
IrunCodeActually(forward(t));
}
move和forward實(shí)現(xiàn)差別不大爬立,但是為了不同用途,有了不同命名万哪。
下面是完美轉(zhuǎn)發(fā)的例子:
void run(int && m) { cout << "rvalue ref" << endl; }
void run(int & m) { cout << "lvalue ref" << endl; }
void run(const int && m) { cout << "const rvalue ref" << endl; }
void run(const int & m) { cout << "const lvalue ref" << endl; }
template <typename T>
void perfectForward(T&& t) {
run(forward<T>(t));
}
int main(){
int a;
int b;
const int c = 1;
const int d = 0;
perfectForward(a);
perfectForward(move(b));
perfectForward(c);
perfectForward(move(d));
return 0;
}
輸出
lvalue ref
rvalue ref
const lvalue ref
const rvalue ref