C++11的新特性具有廣泛的可用性,可以與其他已有的,或者新增的語言特性結合起來進行自由的組合,或者提升已有特性的通用性。
繼承構造函數
C++中的自定義類型--類,具有可派生性,派生類可以自動獲得基類的成員變量和接口(虛函數和純虛函數,這里指的是public派生)。不過基類的非虛函數則無法再被派生類使用了。這條規則對于類中最為特別的構造函數也不例外,如果派生類要使用基類的構造函數,通常需要在構造函數中顯式聲明。
struct A{A(int i){}};
struct B:A{B(int i):A(i)};
B派生于A,B又在構造函數中調用A的構造函數,從而完成構造函數的"傳遞"。在B中有成員的時候:
struct A{A(int i){}};
struct B:A{
B(int i):A(i),d(i){}
int d;
};
派生于結構體A的結構體B擁有一個成員變量d,那么在B的構造函數B(int i)中,我們可以在初始化其基類A的同時初始化成員d。
有的時候,我們的基類可能擁有數量眾多的不同版本的構造函數,而派生類卻只有一些成員函數時,那么對于派生類而言,其構造就等同于構造基類。
在派生類中我們寫的構造函數完完全全就是為了構造基類。那么為了遵從于語法規則,我們還需要寫很多的"透傳"的構造函數。
struct A{
A(int i){}
A(double d,int i){}
A(float f,int i,const char*c){}
//...
};
struct B:A{
B(int i): A(i){}
B(double d, int i): A(d,i){}
B(float f,int i,const char*c): A(f,i,c){}
//...
virtual void ExtraInterface(){}
};
我們的基類A有很多的構造函數的版本,而繼承于A的派生類B實際上只是添加了一個接口ExtraInterface.那么如果我們在構造B的時候想要擁有A這樣多的構造方法的話,就必須一一"透傳"各個接口。
事實上,在C++中已經有了一個好用的規則,就是如果派生類要使用基類的成員函數的話,可以通過using聲明(using-declaration)來完成。
#include<iostream>
using namespace std;
struct Base{
void f(double i){
cout<<"Base:"<<i<<endl;
};
};
struct Derived:Base{
using Base:: f;
void f(int i){
cout<<"Derived:"<<i<<endl;
}
};
int main(){
Base b;
b.f(4.5); //Base:4.5
Derived d;
d.f(4.5); //Base:4.5
}
我們的基類Base和派生類Derived聲明了同名的函數f,不過在派生類中的版本跟基類有所不同。派生類中的f函數接受int類型為參數,而基類中接受double類型的參數。我們這里使用了using 聲明,聲明派生類Derived也使用基類版本的函數f。這樣一來,派生類中實際就擁有了兩個f函數的版本。
C++11中,這個想法被擴展到了構造函數上。子類可以通過使用using聲明來聲明繼承基類的構造函數。
struct A{
A(int i){}
A(double d,int i){}
A(float f,int i,const char*c){}
//...
};
struct B:A{
using A:: A;//繼承構造函數
//...
virtual void ExtraInterface(){}
};
我們通過using A:: A的聲明,把基類中的構造函數悉數繼承到派生類B中。C++11標準繼承構造函數被設計為跟派生類中的各種類默認函數(默認構造、析構、拷貝構造等)一樣,是隱式聲明的。意味著如果一個繼承構造函數不被相關代碼使用,編譯器不會為其產生真正的函數代碼。
不過,繼承構造函數只會初始化基類中成員變量,對于派生類中的成員變量,則無能為力。只能通過初始化一個默認值的方式來解決。如果無法滿足需求的話,只能自己來實現一個構造函數,以達到基類和成員變量都能夠初始化的目的。
基類構造函數的參數會有默認值。對于繼承構造函數來講,參數的默認值是不會被繼承的。事實上,默認值會導致基類產生多個構造函數的版本,這些函數版本都會被派生類繼承。
struct A{
A(int a=3,double=2.4){}
}
struct B:A{
using A:: A;
};
我們的基類的構造函數A(int a=3,double=2.4)有一個接受兩個參數的構造函數,且兩個參數均有默認值。那么A到底有多少個可能的構造函數版本呢?
事實上,B可能從A中繼承來的候選繼承構造函數有如下一些:
A(int=3,double=2.4); 這是使用兩個參數的情況。
A(int=3); 這是減掉一個參數的情況。
A(const A&); 這是默認的復制構造函數。
A(); 這是不使用參數的情況。
相應地,B中的構造函數將會包括以下一些:
B(int,double); 這是一個繼承構造函數。
B(int);這是減少掉一個參數的繼承構造函數。
B(const B&); 這是復制構造函數,這不是繼承來的。
B(); 這是不包含參數默認構造函數。
可以看見,參數默認值會導致多個構造函數版本的產生,因此程序員在使用有參數默認值的構造函數的基類的時候,必須小心。
繼承構造函數“沖突”的情況。通常發生在派生類擁有多個基類的時候。多個基類中的部分構造函數可能導致派生類中的繼承構造函數的函數名、參數(有的時候,我們也稱其為函數簽名)都相同,那么繼承類中的沖突的繼承構造函數將導致不合法的派生類代碼。
struct A{A(int){}};
struct B{B(int){}};
struct C:A,B{
using A:: A;
using B:: B;
};
A和B 的構造函數會導致C中重復定義相同類型的繼承構造函數。可以通過顯式定義繼承類的沖突的構造函數,阻止隱式生成相應的繼承構造函數來解決沖突。比如:
struct C:A,B{
using A:: A;
using B:: B;
C(int){} //其中的構造函數C(int)就很好地解決了繼承構造函數的沖突問題。(為什么能夠解決繼承構造函數從圖的問題。)
};
如果,基類的構造函數被聲明為私有成員函數,或者派生類是從基類中虛繼承的,那么就不能夠在派生類中聲明繼承構造函數。此外,如果一旦使用了繼承構造函數,編譯器就不會再為派生類生成默認構造函數了,程序員必須注意繼承構造函數沒有包含一個無參數的版本。
#include<iostream>
using namespace std;
struct A{
A(int){
}
};
struct B:A{
using A:: A;
};
int main(){
B b;//B沒有默認構造函數
B b(3);//構造函數。
}
委派構造函數
與繼承構造函數類似,委派構造函數也是C++11中對C++的構造函數的一項改進,其目的也是為了減少程序員書寫構造函數的時間。通過委派其他構造函數,多構造函數的類編寫將更加容易。
一個代碼冗余的例子:
#include<iostream>
using namespace std;
class Info{
public:
Info() :type(1), name('a'){ //一次初始化,可以初始化很多變量。
InitRest();
}
Info(int i):type(i), name('a'){
InitRest();
}
Info(char e):type(1), name('e'){
InitRest();
}
private:
void InitRest(){/*其他初始化*/
}
int type;
char name;
};
int main(){
return 0;
}
在代碼中,我們聲明了一個Info的自定義類型。該類型擁有2個成員變量以及3個構造函數。這里的3個構造函數都聲明了初始化列表來初始化成員type和name,并且都調用了相同的InitRest。可以看到,除了初始化列表有的不同,而其他的部分,3個構造函數基本上是相似的,因此其代碼存在著很多重復。
改進方法1:非靜態變量的初始化
#include<iostream>
using namespace std;
class Info{
public:
Info(){
InitRest();
}
Info(int i): type(i){
InitRest();
}
private:
void InitRest(){
}
int type{1};
char name{'a'};
};
雖然構造函數簡單了不少,但是每個構造函數還是需要調用InitRest函數進行初始化。能不能在一些構造函數中連InitRest都不用調用呢?
我們將一個構造函數設定為"基派版本",比如本例中的Info()版本的構造函數,而其他構造函數可以通過委派"基準版本"來進行初始化。
Info(){InitRest();}
Info(int i){this->Info(); type=i;}
Info(char e){this->Info(); name=e;}
我們通過this指針調用我們的"基準版本"的構造函數。但是一般的編譯器都會阻止this->Info()的編譯。原則上,編譯器不允許在構造函數中調用構造函數,即使參數看起來并不相同。
還有一種是用placement new 來強制在本對象地址(this指針所指地址)上調用類的構造函數。這樣,就可以繞過編譯器的檢查,從而在2個構造函數中調用我們的"基準版本"。但是在已經初始化一部分的對象上再次調用構造函數,卻是危險的做法。
在C++11中,我們可以委派構造函數來達到期望的效果。C++11中的委派構造函數是在構造函數的初始化列表位置進行構造的、委派的。
#include<iostream>
using namespace std;
class Info{
public:
Info(){
InitRest();
}
Info(int i): Info(){
type=i;
}
Info(char e): Info(){
name=e;
}
private:
void InitRest(){
}
int type{1};
char name{'a'};
};
在 Info(int) 和 Info(char) 的初始化列表的位置,調用了"基準版本"的構造函數 Info() 。 這里我們為了區分被調用者和調用者,稱在初始化列表中調用"基準版本"的構造函數為委派構造函數,而被調用的"基本版本"則為目標構造函數。在C++11中,所謂委派構造,就是指委派函數將構造的任務委派給了目標構造函數來完成這樣一種類構造的方式。
委派構造函數只能在函數體中為 type、name 等成員賦初值。 這是由于委派構造函數不能有初始化列表造成的。在C++中,構造函數不能同時"委派"和使用初始化列表,所以如果委派構造函數要給變量賦初值,初始化代碼必須放在函數體中。比如:
struct Rule1{
int i;
Rule1(int a):i(a){}
Rule1():Rule1(40),i(1){}//無法通過編譯
Rule1的委派構造函數Rule1() 的寫法就是非法的。我們不能在初始化列表中既初始化成員,為委托其他構造函數完成構造。
(初始化列表的初始化方式總是優于構造函數完成的(實際上在編譯完成時就已經決定了))
稍微改造一下目標構造函數,使得委派構造函數依然可以在初始化列表中初始化所有成員。
class Info{
public:
Info():Info(1,'a'){}
Info(int i): Info(i,'a'){}
Info(char e): Info(1,e){}
private:
Info(int i,char e): type(i), name(e){/*其他初始化*/}
int type;
char name;
//...
};
我們定義了一個私有的目標構造函數Info(int,char), 這個構造函數接受兩個參數,并將參數在初始化列表中初始化。由于這個目標構造函數的存在,我們可以不再需要InitRest函數了,而是將其代碼都放入Info(int,char)中。這樣,其他委派構造函數就可以委派該目標構造函數來完成構造。
在使用委派構造函數的時候,我們建議程序員抽象出最為"通用"的行為做目標構造函數。這樣做一來代碼清晰,二來行為也更加正確。由于在C++11中,目標構造函數的執行總是先于委派構造函數而造成的。因此避免目標構造函數和委派構造函數體中初始化同樣的成員通常是必要的,
在構造函數比較多的時候,我們可能會擁有不止一個委派構造函數,而一些目標構造函數很可能也是委派構造函數,這樣一來,我們就可以在委派構造函數中形成鏈狀的委派構造關系。
class Info{
public:
Info(): Info(1){} //委派構造函數
Info(int i): Info(i,'a'){} //即是目標構造函數,也是委派構造函數
Info(char e): Info(1,e){}
private:
Info(int i,char e): type(i), name(e){/*其他初始化*/}//目標構造函數
int type;
char name;
};
鏈狀委托構造,這里我們使Info() 委托Info(int)進行構造,而Info(int)又委托Info(int,char)進行構造。在委托構造的鏈狀關系中,就是不能形成委托環。比如:
struct Rule2{
int i,c;
Rule2():Rule2(2){}
Rule2(int i):Rule2('c'){}
Rule2(char c):Rule2(2){}
};
Rule2定義中,Rule2()、Rule2(int)和Rule2(char)都依賴于別的構造函數,形成環委托構造關系。這樣的代碼通常會導致編譯錯誤。委托構造的一個很實際的應用就是使用構造模板函數產生目標構造函數。
#include<list>
#include<vector>
#include<deque>
using namespace std;
class TDConstructed{
template<class T>TDConstructed(T first, T last):l(first,last){} //盡可能還是多理解這個地方
list<int> l;
public:
TDConstructed(vector<short> &v):TDConstructed(v.begin(),v.end()){}
TDConstructed(deque<int> &d):TDConstructed(d.begin(),d.end()){}
};
我們定義了一個構造函數模板。通過兩個委派構造函數的委托,構造函數模板會被實例化。T會分別被推導為 vector<short>::iterator 和 deque<int>::iterator 兩種類型。這樣一來, 我們的TDConstructed類就可以很容易地接受多種容器對其進行初始化。
(委托構造使得構造函數的泛型編程成為了一種可能)
在異常處理方面,如果在委派構造函數中使用try的話,那么從目標構造函數中產生的異常,都可以在委派構造函數中被捕捉到。我們看下面的例子:
#include <iostream>
using namespace std;
class DCExcept{
public:
DCExcept(double d)
try: DCExcept(1,d){
cout<<"Run the body."<<endl;
//其他初始化
}
catch(...){
cout<<"caught exception."<<endl;
}
private:
DCExcept(int i,double d){
cout<<"going to throw!"<endl;
throw 0; //拋出異常。
}
int type;
double data;
};
int main(){
DCExcept a(1.2);
}
我們在目標構造函數DCException(int,double)跑出了一個異常,并在委派構造函數DCExcept(int)中進行了捕捉。而委派構造函數的函數體部分的代碼并沒有被執行。這樣的設計是合理的,因為如果函數體依賴于目標構造函數構造的結果,那么當目標構造函數構造發生異常的情況下,還是不要執行委派構造函數函數體中的代碼為好。
右值引用:移動語義和完美轉發
指針成員與拷貝構造
對C++程序員來說,編寫C++程序有一條必須注意的規則,就是在類中包含了一個指針成員的話,那么就要特別小心拷貝構造函數的編寫,因為一不小心,就會出現內存泄露。
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(0)){}
~HasPtrMem() {
delete d;
}
int *d; //指針成員d
};
int main(){
HasPtrMem a;
HasPtrMem b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}
我們定義了一個HasPtrMem的類。這個類包含一個指針成員,該成員在構造時接受一個new操作分配堆內存返回的指針,而在析構的時候則會被delete操作用于釋放之前分配的堆內存。在main函數中,我們聲明了HsaPtrMem類型的變量a,又使用a初始化了變量b。按照C++語法,這會調用HasPtrMem的拷貝構造函數。(這里的拷貝構造函數由編譯器隱式生成,其作用是執行類似于memcpy的按位拷貝。這樣的構造方式有一個問題,就是a.d和b.d都指向同一塊堆內存。因此在main作用域結束的時候,a和b的析構函數紛紛被調用,當其中之一完成析構之后(比如b),那么a.d就成了一個"懸掛指針",因為其不再指向有效的內存了。那么在該懸掛指針上釋放內存就會造成嚴重的錯誤。
這樣的拷貝方式,在C++中也常被稱為"淺拷貝"。而在為聲明構造函數的情況下,C++也會為類生成一個淺拷貝的構造函數。通常最佳的解決方案是用戶自定義拷貝構造函數來實現"深拷貝":
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(0)){
cout<<"Construct:"<<endl;
}
HasPtrMem(HasPtrMem&h): d(new int(*h.d)){
cout<<"Copy construct:"<<endl;
} //拷貝構造函數,從堆中分配內存,并用*h.d初始化
~HasPtrMem() {
delete d;
}
int *d; //指針成員d
};
int main(){
HasPtrMem a;
HasPtrMem b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}
(問題:淺拷貝和深拷貝 的差別)
我們為HasPtrMem添加了一個拷貝構造函數。拷貝構造函數從堆中分配內存,將該分配來的內存的指針交還給d, 又使用*(h.d)對 *d進行了初始化。通過這樣的方法,就避免了懸掛指針的困擾。
拷貝構造函數中為指針成員分配新的內存再進行內容拷貝的做法在C++編程中幾乎被視為不可違背的。不過在一些時候,我們確實不需要這樣的拷貝語義。
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(0)){
cout<<"Construct:" << ++n_cstr<<endl;
}
HasPtrMem(const HasPtrMem&h): d(new int(*h.d)){
cout<<"Copy construct:"<< ++n_cptr<<endl;
} //拷貝構造函數,從堆中分配內存,并用*h.d初始化
~HasPtrMem() {
cout<<"Destruct:"<<++n_dstr<<endl;
}
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 a=GetTemp();
}
(回顧:靜態變量和非靜態變量)
數據成員可以分靜態變量、非靜態變量兩種.
靜態成員:靜態類中的成員加入static修飾符,即是靜態成員.可以直接使用類名+靜態成員名訪問此靜態成員,因為靜態成員存在于內存,非靜態成員需要實例化才會分配內存,所以靜態成員不能訪問非靜態的成員..因為靜態成員存在于內存,所以非靜態成員可以直接訪問類中靜態的成員.
非成靜態員:所有沒有加Static的成員都是非靜態成員,當類被實例化之后,可以通過實例化的類名進行訪問..非靜態成員的生存期決定于該類的生存期..而靜態成員則不存在生存期的概念,因為靜態成員始終駐留在內容中..
我們聲明了一個返回一個HasPtrMem變量的函數。為了記錄構造函數、拷貝構造函數,以及析構函數調用的次數,我們用了一些靜態變量。在main函數中,我們簡單地聲明了一個HasPtrMem的變量a,要求它使用GetTemp的返回值進行初始化。
//正常情況下的輸出:
Construct:1
Copy construct:1 //這個是臨時對象的構造
Destruct:1 //這個應該是臨時對象的析構
Copy construct:2
Destruct:2
Destruct:3
但是在C++11或者非C++里面的結果
只是一個淺拷貝
這里的構造函數被調用了一次,是GetTemp函數中HasPtrMem()表達式顯示地調用了構造函數而打印出來的。而拷貝構造函數則被調用了兩回。一次是從GetTemp函數中HasPtrMem()生成的變量上拷貝構造出來一個臨時值,以用做GetTemp的返回值,而另一次則是由臨時值構造出main中變量a調用的。對應的,析構函數也就調用了3次。

最頭疼的就是拷貝構造函數的調用。在上面的代碼上,類HasPtrMem只有一個Int類型的指針。如果HasPtrMem的指針指向非常大的堆內存數據的話,那么拷貝構造函數就會非常昂貴。可以想象,一旦這樣,a的初始化表達式的執行速度非常慢。臨時變量的產生和銷毀以及拷貝的發生對于程序員來說基本上是透明的,不會影響程序的正常值,因而即使該問題導致程序的性能不如預期,也不易被程序員察覺(事實上,編譯器常常對函數返回值有專門的優化)
然后,按照C++的語義,臨時對象將在語句結束后被析構,會釋放它所包含的堆內存資源。而a在拷貝構造的時候,又會被分配堆內存。這樣意義不大,所以,考慮在臨時對象構造a的時候不分配內存,即不使用拷貝構造。
剩下的就是移動構造:

上半部分從臨時變量中拷貝構造變量a的做法,即在拷貝時分配新的堆內存,并從臨時對象的堆內存中拷貝內容至a.d。而構造完成后,臨時對象將析構,因此,其擁有的堆內存資源會被析構函數釋放。
下半部分,在構造函數時使得a.d指向臨時對象的堆內存資源。同時我們保證臨時對象不釋放所指向的堆內存,那么,在構造完成后,臨時對象被析構,a就從中"偷"到了臨時對象所擁有的堆內存資源。
在 C++11 中,這樣的"偷走"臨時變量中資源的構造函數,就被稱為"移動構造函數"。
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(3)){
cout<<"Construct:" << ++n_cstr<<endl;
}
HasPtrMem(const HasPtrMem&h): d(new int(*h.d)){
cout<<"Copy construct:"<< ++n_cptr<<endl;
} //拷貝構造函數,從堆中分配內存,并用*h.d初始化
HasPtrMem(HasPtrMem &&h):d(h.d){
h.d=nullptr;//將臨時值得指針成員置空。
cout<<"Move construct:"<<++n_mvtr<<endl;
}
~HasPtrMem() {
delete d;
cout<<"Destruct:"<<++n_dstr<<endl;
}
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(){
HasPtrMem h;
cout<<"Resource from"<<__func__<<":"<<hex<<h.d<<endl;
return h;
}
int main(){
//HasPtrMem b;
HasPtrMem a=GetTemp();
cout<<"Resource from"<<__func__<<":"<<hex<<a.d<<endl;
}
這里其實,就多了一個構造函數HasPtrMem(HasPtrMem&&), 這個就是我們所謂的移動構造函數。與拷貝構造函數不同的是,移動構造函數接受一個所謂的"右值引用"的參數,關于右值,讀者可以暫時理解為臨時變量的引用。移動構造函數使用了參數h的成員d初始化了本對象的成員d(而不是像拷貝構造函數一樣需要分配內存,然后將內存一次拷貝到新分配的內存中),隨后h的成員d置為指針空值nullptr。完成了移動構造函數的全過程。
所謂的偷堆內存,就是指將本對象d指向h.d所指的內存這一條語句,相應的,我們還將h的成員d置為指針空值。
//理論上的結果:
Construct:1
Resource from GetTemp:0x603010
Move construct:1
Destruct:1
Move construct:2
Destruct:2
Resource from main:0x603010
Destruct:3
//實際上的結果:似乎只要涉及到需要臨時變量的生成的時候,都會有問題。
Construct:1
Resource from GetTemp:0x603010
Resource from main:0x603010
Destruct:1
如果堆內存不是一個int長度的數據,而是以MBty為單位的堆空間,那么這樣的移動帶來的性能提升是非常驚人的。
如果傳了引用或者指針到函數里面作為參數,效果雖然不差。但是從使用的方便性上來看效果卻不好,如果函數返回臨時值的話,可以在單條語句里面完成很多計算,比如可以很自然地寫出如下語句:
Caculate(GetTemp(), SomeOther(Maybe(),Useful(Values,2)));
但如果通過傳引用或者指針的方法而不返回值的話,通常就需要很多語句來完成上面的工作。
string*a; vector b;//事先聲明一些變量用于傳遞返回值
...
Useful(Values,2,a);//最后一個參數是指針,用于返回結果
SomeOther(Maybe(),a,b);//最后一個參數是引用,用于返回結果
Caculate(GetTemp(), b);
當聲明這些傳遞返回值的變量為全局的,函數再將這些引用和指針作為返回值返回給調用者,我們也需要Caculate調用之前聲明好所有的引用和指針。函數返回臨時變量的好處就是不需要聲明變量,也不需要知道生命期。程序員只需要按照最自然的方式,使用最簡單語句就可以完成大量的工作。
然后,移動語義何時會被觸發。之前我們只是提到了臨時對象,一旦我們用到的是個臨時變量,那么移動構造語義就可以得以執行。**那么,在C++中如何判斷產生了臨時對象?如何將其用于移動構造函數?是否只有臨時變量可以用于移動構造?.....
在C++98/03的語言和庫中,以及存在了一些移動語義相關的概念:
A. 智能指針的拷貝(auto_ptr "copy")
B. 鏈表拼接(list::splice)
c. 容器內的置換(swap on containers)
這些操作都包含了從一個對象到另一個對象的資源轉移的過程,唯一欠缺的是統一的語法和語義的支持,來使我們可以使用通用的代碼移動任意的對象。如果能夠任意地使用對象的移動,而不是拷貝,那么標準庫中的很多地方的性能都會大大提高。
左值、右值與右值引用
在C語言中,我們常常會提起左值(lvalue)、右值(rvalue),編譯器報出的錯誤信息里面有時也會包含左值、右值的說法。不過,左值、右值通常不是通過一個嚴謹的定義而為人所知的,大多數時候左右值的定義與其判別方法是一體的。一個最典型的判別方法就是,在賦值表達式中,出現在等號左邊的就是"左值",而在等號右邊的,則稱為"右值"。不過C++中,有一個被廣泛認同的說法,那就是可以取值的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值。更為細致地,在C++11中,右值是由兩個概念構成得,一個是將亡值,另一個則是純右值。
純右值就是C++98標準中右值的概念,講的是用于辨別臨時變量和一些不跟對象關聯的值。比如非引用返回的函數返回的臨時變量值,就是一個純右值。一些運算表達式,比如1+3產生的臨時變量值,也是純右值。而不跟對象關聯的字面量值,比如:2、'c'、true,也是純右值。此外,類型轉換函數的返回值、lambda表達式等,也都是右值。
而將亡值則是C++11新增的跟右值引用相關的表達式,這樣表達式通常是將要被移動的對象。比如返回右值引用T&&的函數返回值、std::move的返回值,或者轉換為T&&的類型轉換函數的返回值。而剩下的,可以標識函數、對象的值都屬于左值。在C++11的程序中,所有的值比屬于左值、將亡值、純右值三者之一。
在C++11中,右值引用就是對一個右值進行引用的類型。事實上,由于右值通常不具有名字,我們也只能通過引用的方式找到它的存在。通常情況下,我們只能是從右值表達式獲得其引用。比如:
T&&a = ReturnRvalue();
假設ReturnRvalue返回一個右值,我們就聲明了一個名為a的右值引用,其值等于ReturnRvalue函數返回的臨時變量的值。
右值引用和左值引用都是屬于引用類型。無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。原因可以理解為是引用類型本身自己并不擁有所綁定對象的內存,只是該對象的一個別名。左值引用是具名變量值的別名,而右值引用則是不具名(匿名)變量的別名。(也就是說需要找一個寄主)
(問題:我可不可以理解為移動構造函數比拷貝構造函數更適合右值。所以對于對應的右值,移動構造函數更容易被匹配到。)
通常情況下,右值引用是不能夠綁定到任何的左值的。
int c
int &&d=c
相對地,在C++98標準中就已經出現的左值引用是否可以綁定到右值(由右值進行初始化)?
T&e = ReturnRvalue();
const T&f = ReturnRvalue();
這里一共有11個特性,先只學到這里,因為我需要先對C++先過一遍,然后,再回來看右值和強制轉換的部分。