【1】左值與右值
在C語言中,常常會提起左值(lvalue)、右值(rvalue)這樣的稱呼。
而在編譯程序時,編譯器有時也會在報出的錯誤信息中包含左值、右值的說法。
不過左值、右值通常不是通過一個嚴謹的定義而為人所知的。
事實上,之所以只知道一些關於左值、右值的判斷而很少聽到其真正的定義的一個原因就是很難歸納。而且即使歸納了,仍需要大量的解釋。
(1)左右值的判別方法一
大多數時候,左右值的一個最為典型的判別方法:
在賦值表達式中,出現在等號左邊的就是“左值”,而在等號右邊的,則稱為“右值”。比如:
a = b + c;
在這個賦值表達式中,a就是一個左值,而b+c則是一個右值。
這種識別左值、右值的方法在C++中依然有效。
(2)左右值判別方法二
不過C++中還有一個被廣泛認同的說法,那就是:
可以取地址的、有名字的就是左值,反之,不能取地址的、沒有名字的就是右值。
那么這個加法賦值表達式中,&a是允許的操作,但&(b + c)這樣的操作則不會通過編譯。
因此a是一個左值,(b+c)是一個右值。這些判別方法通常都非常有效。
(3)左右值判別方法三
更為細致地,在C++11中,右值是由兩個概念構成的,一個是純右值(prvalue,Pure Rvalue),另一個則是將亡值(xvalue,eXpiring Value)。
其中純右值就是C++98標准中右值的概念,指的是用於辨識臨時變量和一些不跟對象關聯的值。
比如:返回類型為非引用的函數返回的臨時變量值就是一個純右值。
一些運算表達式,比如1+3產生的臨時變量值,也是純右值。
而不跟對象關聯的字面量值,比如:2、‘c’、true,也是純右值。
此外,類型轉換函數的返回值、lambda表達式等,也都是右值。
而將亡值則是C++11新增的跟右值引用相關的表達式,這樣表達式通常是將要被移動的對象(移為他用),比如:
[1] 返回右值引用T&&的函數返回值
[2] std::move的返回值
[3] 轉換為T&&的類型轉換函數的返回值。
而剩余的,可以標識函數、對象的值都屬於左值。
所以,在C++11的程序中,所有的值必屬於左值、將亡值、純右值三者之一。
【2】左值引用與右值引用
在C++11中,右值引用(rvalue reference)就是對一個右值進行引用的類型。
為了區別於之前C++98中的引用類型,我們稱C++98中的引用為“左值引用”(lvalue reference)。
右值引用和左值引用都是屬於引用類型,無論是聲明一個左值引用還是右值引用,都必須立即進行初始化。
其原因可以理解為是引用類型本身自己並不擁有所綁定對象的內存,只是該對象的一個別名。
左值引用是具名變量值的別名,而右值引用則是不具名(匿名)變量的別名。
事實上,由於右值通常不具有名字,我們也只能通過引用的方式找到它的存在。
通常情況下,我們只能是從右值表達式獲得其引用。比如:
T && a = ReturnRvalue();
這個表達式中,假設ReturnRvalue返回一個右值,我們就聲明了一個名為a的右值引用,其值等於ReturnRvalue函數返回的臨時變量的值。
ReturnRvalue函數返回的右值在表達式語句結束后,其生命也就終結了(通常我們也稱其具有表達式生命期)。
而通過右值引用的聲明,該右值又“重獲新生”,其生命期將與右值引用類型變量a的生命期一樣。
即就是,只要a還“活着”,該右值臨時量將會一直“存活”下去。所以,相比於以下語句的聲明方式:
T b = ReturnRvalue();
剛才的右值引用變量聲明,就會少一次對象的析構及一次對象的構造。
因為a是右值引用,直接綁定了ReturnRvalue()返回的臨時量,而b只是由臨時值構造而成的,而臨時量在表達式結束后會被析構掉,因應就會多一次析構和構造的開銷。
關於右值引用,可以參考下例加深理解:
1 #include <iostream> 2 3 void f(int& i) { std::cout << "call left value ref :: " << i << "\n"; } 4 void f(int&& i) { std::cout << "call right value ref :: " << i << "\n"; } 5 6 int main() 7 { 8 int i = 2020; 9 f(i); // call left value ref :: 2020 10 f(1010); // call right value ref :: 1010 11 f(std::move(i)); // call right value ref :: 2020 12 13 system("pause"); 14 return 0; 15 } 16 17 /* 18 call left value ref :: 2020 19 call right value ref :: 1010 20 call right value ref :: 2020 21 請按任意鍵繼續. . . 22 */
【3】常量左值引用 與 常量右值引用
(1)常量左值引用
通常情況下,右值引用是不能夠綁定到任何的左值的。比如下面的表達式就是無法通過編譯的:
int c; int && d = c;
那么,在C++98標准中就已經出現的左值引用是否可以綁定到右值(由右值進行初始化)呢?比如:
T & e = ReturnRvalue(); const T & f = ReturnRvalue();
這樣的語句是否能夠通過編譯呢?答案是:e的初始化會導致編譯時錯誤,而f則不會。出現這樣的狀況的原因:
常量左值引用在C++98標准中開始就是個“萬能”的引用類型。它可以接受非常量左值、常量左值、右值對其進行初始化。
而且,在使用右值對其初始化的時候,常量左值引用還可以像右值引用一樣將右值的生命期延長。
不過相比於右值引用所引用的右值,常量左值所引用的右值在它的“余生”中只能是只讀的。
相對地,非常量左值只能接受非常量左值對其進行初始化。
(2)常量右值引用
為了語義的完整,C++11中還存在着常量右值引用。比如通過以下代碼聲明一個常量右值引用:
const T && crvalueref = ReturnRvalue();
但是,一來右值引用主要就是為了移動語義,而移動語義需要右值是可以被修改的,那么常量右值引用在移動語義中就沒有用武之處;
二來如果要引用右值且讓右值不可以更改,常量左值引用往往就足夠了。
因此,在現在的尷尬情況下,還沒有看到常量右值引用有何用處。
(3)引用類型可引用值的類型表
下面列出了在C++11中各種引用類型可以引用的值的類型:

值得注意的是,只要能夠綁定右值的引用類型,都能夠延長右值的生命期。
【4】如何判斷?
有的時候,我們可能不知道一個類型是否是引用類型,以及是左值引用還是右值引用(這在模板中比較常見)。
標准庫在<type_traits>頭文件中提供了3個模板類:is_rvalue_reference、is_lvalue_reference、is_reference,可供我們進行判斷。比如:
1 #include <iostream> 2 #include <type_traits> 3 using namespace std; 4 5 int main() 6 { 7 cout << is_reference<int>::value << endl; // 0 8 cout << is_rvalue_reference<string &&>::value << endl; // 1 9 cout << is_lvalue_reference<string &> ::value << endl; // 1 10 cout << is_rvalue_reference<const string &&>::value << endl; // 1 11 cout << is_lvalue_reference<const string &> ::value << endl; // 1 12 }
我們通過模板類的成員value就可以打印出stirng && 是否是一個右值引用了。
配合C++11中的類型推導操作符decltype,我們甚至還可以對變量的類型進行判斷。
good good study, day day up.
順序 選擇 循環 總結
