Attention:this blog is a translation of https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c ,which is posted by @internalpoiners.
一耗跛、前言
一直以來毒嫡,我都對(duì)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):這完全合法。
對(duì)于左值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),所以我們會(huì)把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ù)肯定會(huì)拋出錯(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ù)〔渑妫現(xiàn)在,我們看看如果函數(shù)返回一個(gè)左值章鲤,這樣的賦值會(huì)發(fā)生什么變化摊灭。看下面的代碼片段(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í)會(huì)很有作用辜妓,這些知識(shí)會(huì)在未來的章節(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ā)生了什么腥例?答案很簡(jiǎn)單: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è)對(duì)y
的引用借跪,它被稱作左值引用(lvalue reference)≌海現(xiàn)在你可以開心地通過該引用改變y
的值了。
我們知道掏愁,一個(gè)引用必須只想一個(gè)具體的內(nèi)存位置中的一個(gè)已經(jīng)存在的對(duì)象歇由,即一個(gè)左值。這里y
確實(shí)存在果港,所以代碼運(yùn)行完美沦泌。
現(xiàn)在,如果我縮短整個(gè)過程辛掠,嘗試將10
直接賦值給我的引用谢谦,并且沒有任何對(duì)象持有該引用,將會(huì)發(fā)生什么公浪?
int& yref = 10; // will it work?
在右邊我們有一個(gè)臨時(shí)值他宛,一個(gè)需要被存儲(chǔ)在一個(gè)左值中的右值。在左邊我們有一個(gè)引用(一個(gè)左值)欠气,他應(yīng)該指向一個(gè)已經(jīng)存在的對(duì)象。但是10
是一個(gè)數(shù)字常量(numeric constant)镜撩,也就是一個(gè)左值预柒,將它賦給引用與引用所表述的精神沖突。
如果你仔細(xì)想想袁梗,那就是被禁止的從右值到左值的轉(zhuǎn)換宜鸯。一個(gè)volitile
的數(shù)字常量(右值)如果想要被引用,需要先變成一個(gè)左值遮怜。如果那被允許淋袖,你就可以通過它的引用來改變數(shù)字常量的值。相當(dāng)沒有意義锯梁,不是嗎即碗?更重要的是,一旦這些值不再存在這些引用該指向哪里呢陌凳?
下面的代碼片段同樣會(huì)發(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í)的變量來存儲(chǔ)右值,然后將變量傳入函數(shù)中(就像注釋中寫的那樣)。將一個(gè)數(shù)字傳入一個(gè)函數(shù)確實(shí)不太方便保檐。
六耕蝉、常量左值引用
先看看GCC對(duì)于之前兩個(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
的并且會(huì)很快失效(expire),所以給他一個(gè)引用是沒什么意義的砸王。如果我們讓引用本身變成常量引用推盛,那樣的話該引用指向的值就不能被改變了。現(xiàn)在右值被修改的問題被很好地解決了谦铃。同樣耘成,這不是一個(gè)技術(shù)限制,而是C ++人員為避免愚蠢麻煩所作的選擇驹闰。
應(yīng)用:C++中經(jīng)常通過常量引用來將值傳入函數(shù)中瘪菌,這避免了不必要的臨時(shí)對(duì)象的創(chuàng)建和拷貝。
編譯器會(huì)為你創(chuàng)建一個(gè)隱藏的變量(即一個(gè)左值)來存儲(chǔ)初始的字面常量嘹朗,然后將隱藏的變量綁定到你的引用上去师妙。那跟我之前的一組代碼片段中手動(dòng)完成的是一碼事,例如:
// 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)一步推動(dòng)了右值的限定蓄诽,引入了右值引用(rvalue reference)和移動(dòng)(move semantics)的概念。這些將在下一篇文章中介紹媒吗。