理解C++中的左值和右值

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

等一下,xy是左值,但是加法操作符需要右值作為參數(shù):發(fā)生了什么?答案很簡單:xy經(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)直接的,字面常量10volatile的并且會很快失效(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)的概念。這些將在下一篇文章中介紹。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 襟懷先后論,① 詠記岳晹樓。 處世無良策,② 經(jīng)邦有好謀。③ 生前民已識, 死后帝方愁。 更誦公文意, 憑欄熱淚流...
    常將三俠閱讀 953評論 22 18
  • 作文導(dǎo)師團(tuán)溜溜梅“天天圖說”第 一天:小作家樂樂。題目吃蘋果:在一個(gè)陽光明媚的早晨,小紅去果園摘了一藍(lán)又大又圓的蘋...
    冰魚燕閱讀 272評論 0 0
  • 陽光溫暖著大地 柳條輕輕搖弋 微風(fēng)蕩漾著湖面 獨(dú)自一人屹立在高樓的窗邊 看著路上過往的人群、有多少人帶著空虛的靈魂...
    喚醒人生閱讀 255評論 0 1
  • 選擇器是jQuery的根基,在jQuery中,對事件處理,遍歷DOM和Ajax操作都依賴于選擇器。--<鋒利的jq...
    匿名用戶404閱讀 317評論 0 4

友情鏈接更多精彩內(nèi)容