Attention:this blog is a translation of https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c ,which is posted by @internalpoiners.
一、前言
一直以來,我都對C++中左值(lvalue)和右值(lvalue)的概念模糊不清。我認(rèn)為是時(shí)候好好理解他們了,因?yàn)檫@些概念隨著C++語言的進(jìn)化變得越來越重要。
二、左值和右值——一個(gè)友好的定義
首先,讓我們避開那些正式的定義。在C++中,一個(gè)左值是指向一個(gè)指定內(nèi)存的東西。另一方面,右值就是不指向任何地方的東西。通常來說,右值是暫時(shí)和短命的,而左值則活的很久,因?yàn)樗麄円宰兞康男问剑╲ariable)存在。我們可以將左值看作為容器(container)而將右值看做容器中的事物。如果容器消失了,容器中的事物也就自然就無法存在了。
讓我們現(xiàn)在來看一些例子:
int x = 666; //ok
在這里,666是一個(gè)右值。一個(gè)數(shù)字(從技術(shù)角度來說他是一個(gè)字面常量(literal constant))沒有指定的內(nèi)存地址,當(dāng)然在程序運(yùn)行時(shí)一些臨時(shí)的寄存器除外。在該例中,666被賦值(assign)給x,x是一個(gè)變量。一個(gè)變量有著具體(specific)的內(nèi)存位置,所以他是一個(gè)左值。C++中聲明一個(gè)賦值(assignment)需要一個(gè)左值作為它的左操作數(shù)(left operand):這完全合法。
對于左值x,你可以做像這樣的操作:
int* y = &x; //ok
在這里我通過取地址操作符&獲取了x的內(nèi)存地址并且把它放進(jìn)了y。&操作符需要一個(gè)左值并且產(chǎn)生了一個(gè)右值,這也是另一個(gè)完全合法的操作:在賦值操作符的左邊我們有一個(gè)左值(一個(gè)變量),在右邊我們使用取地址操作符產(chǎn)生的右值。
然而,我們不能這樣寫:
int y;
666 = y; //error!
可能上面的結(jié)論是顯而易見的,但是從技術(shù)上來說是因?yàn)?code>666是一個(gè)字面常量也就是一個(gè)右值,它沒有一個(gè)具體的內(nèi)存位置(memory location),所以我們會把y分配到一個(gè)不存在的地方。
下面是GCC給出的變異錯(cuò)誤提示:
error: lvalue required as left operand of assignment
賦值的左操作數(shù)需要一個(gè)左值,這里我們使用了一個(gè)右值666。
我們也不能這樣做:
int* y = &666;// error~
GCC給出了以下錯(cuò)誤提示:
error: lvalue required as unary '&' operand`
&操作符需要一個(gè)左值作為操作數(shù),因?yàn)橹挥幸粋€(gè)左值才擁有地址。
三、返回左值和右值的函數(shù)
我們知道一個(gè)賦值的左操作數(shù)必須是一個(gè)左值,因此下面的這個(gè)函數(shù)肯定會拋出錯(cuò)誤:lvalue required as left operand of assignment
int setValue()
{
return 6;
}
// ... somewhere in main() ...
setValue() = 3; // error!
錯(cuò)誤原因很清楚:setValue()返回了一個(gè)右值(一個(gè)臨時(shí)值6),他不能作為一個(gè)賦值的左操作數(shù)?,F(xiàn)在,我們看看如果函數(shù)返回一個(gè)左值,這樣的賦值會發(fā)生什么變化??聪旅娴拇a片段(snippet):
int global = 100;
int& setGlobal()
{
return global;
}
// ... somewhere in main() ...
setGlobal() = 400; // OK
該程序可以運(yùn)行,因?yàn)樵谶@里setGlobal()返回一個(gè)引用(reference),跟之前的setValue()不同。一個(gè)引用是指向一個(gè)已經(jīng)存在的內(nèi)存位置(global變量)的東西,因此它是一個(gè)左值,所以它能被賦值。注意這里的&:它不是取地址操作符,他定義了返回的類型(一個(gè)引用)。
可以從函數(shù)返回左值看上去有些隱晦,它在你做一些進(jìn)階的編程例如實(shí)現(xiàn)一些操作符的重載(implementing overload operators)時(shí)會很有作用,這些知識會在未來的章節(jié)中講述。
四、左值到右值的轉(zhuǎn)換
一個(gè)左值可以被轉(zhuǎn)換(convert)為右值,這完全合法且經(jīng)常發(fā)生。讓我們先用+操作符作為一個(gè)例子,根據(jù)C++的規(guī)范(specification),它使用兩個(gè)右值作為參數(shù)并返回一個(gè)右值(譯者按:可以將操作符理解為一個(gè)函數(shù))。
讓我們看下面的代碼片段:
int x = 1;
int y = 3;
int z = x + y; // ok
等一下,x和y是左值,但是加法操作符需要右值作為參數(shù):發(fā)生了什么?答案很簡單:x和y經(jīng)歷了一個(gè)隱式(implicit)的左值到右值(lvalue-to-rvalue)的轉(zhuǎn)換。許多其他的操作符也有同樣的轉(zhuǎn)換——減法、加法、除法等等。
五、左值引用
相反呢?一個(gè)右值可以被轉(zhuǎn)化為左值嗎?不可以,它不是技術(shù)所限,而是C++編程語言就是那樣設(shè)計(jì)的。
在C++中,當(dāng)你做這樣的事:
int y = 10;
int& yref = y;
yref++; // y is now 11
這里將yref聲明為類型int&:一個(gè)對y的引用,它被稱作左值引用(lvalue reference)。現(xiàn)在你可以開心地通過該引用改變y的值了。
我們知道,一個(gè)引用必須只想一個(gè)具體的內(nèi)存位置中的一個(gè)已經(jīng)存在的對象,即一個(gè)左值。這里y確實(shí)存在,所以代碼運(yùn)行完美。
現(xiàn)在,如果我縮短整個(gè)過程,嘗試將10直接賦值給我的引用,并且沒有任何對象持有該引用,將會發(fā)生什么?
int& yref = 10; // will it work?
在右邊我們有一個(gè)臨時(shí)值,一個(gè)需要被存儲在一個(gè)左值中的右值。在左邊我們有一個(gè)引用(一個(gè)左值),他應(yīng)該指向一個(gè)已經(jīng)存在的對象。但是10 是一個(gè)數(shù)字常量(numeric constant),也就是一個(gè)左值,將它賦給引用與引用所表述的精神沖突。
如果你仔細(xì)想想,那就是被禁止的從右值到左值的轉(zhuǎn)換。一個(gè)volitile的數(shù)字常量(右值)如果想要被引用,需要先變成一個(gè)左值。如果那被允許,你就可以通過它的引用來改變數(shù)字常量的值。相當(dāng)沒有意義,不是嗎?更重要的是,一旦這些值不再存在這些引用該指向哪里呢?
下面的代碼片段同樣會發(fā)生錯(cuò)誤,原因跟剛才的一樣:
void fnc(int& x)
{
}
int main()
{
fnc(10); // Nope!
// This works instead:
// int x = 10;
// fnc(x);
}
我將一個(gè)臨時(shí)值10傳入了一個(gè)需要引用作為參數(shù)的函數(shù)中,產(chǎn)生了將右值轉(zhuǎn)換為左值的錯(cuò)誤。這里有一個(gè)解決方法(workaround),創(chuàng)造一個(gè)臨時(shí)的變量來存儲右值,然后將變量傳入函數(shù)中(就像注釋中寫的那樣)。將一個(gè)數(shù)字傳入一個(gè)函數(shù)確實(shí)不太方便。
六、常量左值引用
先看看GCC對于之前兩個(gè)代碼片段給出的錯(cuò)誤提示:
error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
GCC認(rèn)為引用不是const的,即一個(gè)常量。根據(jù)C++規(guī)范,你可以將一個(gè)const的左值綁定到一個(gè)右值上,所以下面的代碼可以成功運(yùn)行:
const int& ref = 10; // OK!
當(dāng)然,下面的也是:
void fnc(const int& x)
{
}
int main()
{
fnc(10); // OK!
}
背后的道理是相當(dāng)直接的,字面常量10是volatile的并且會很快失效(expire),所以給他一個(gè)引用是沒什么意義的。如果我們讓引用本身變成常量引用,那樣的話該引用指向的值就不能被改變了。現(xiàn)在右值被修改的問題被很好地解決了。同樣,這不是一個(gè)技術(shù)限制,而是C ++人員為避免愚蠢麻煩所作的選擇。
應(yīng)用:C++中經(jīng)常通過常量引用來將值傳入函數(shù)中,這避免了不必要的臨時(shí)對象的創(chuàng)建和拷貝。
編譯器會為你創(chuàng)建一個(gè)隱藏的變量(即一個(gè)左值)來存儲初始的字面常量,然后將隱藏的變量綁定到你的引用上去。那跟我之前的一組代碼片段中手動完成的是一碼事,例如:
// the following...
const int& ref = 10;
// ... would translate to:
int __internal_unique_name = 10;
const int& ref = __internal_unique_name;
現(xiàn)在你的引用指向了真實(shí)存在的事物(知道它走出作用域外)并且你可以正常使用它,出克改變他指向的值。
const int& ref = 10;
std::cout << ref << "\n"; // OK!
std::cout << ++ref << "\n"; // error: increment of read-only reference ‘ref’
七、結(jié)論
理解左值和右值的含義讓我弄清楚了幾個(gè)C++內(nèi)在的工作方式。C++11進(jìn)一步推動了右值的限定,引入了右值引用(rvalue reference)和移動(move semantics)的概念。這些將在下一篇文章中介紹。