理解 C/C++ 中的左值和右值
本文翻譯自 Eli Bendersky’s website。
原文:ofollow,noindex">Understanding lvalues and rvalues in C and C++
翻譯者:nettee
我們在 C/C++ 程式設計中並不會經常用到左值 (lvalue)
和右值 (rvalue)
兩個術語。然而一旦遇見,又常常不清楚它們的含義。最可能出現兩這個術語的地方是在編譯錯誤或警告的資訊中。例如,使用gcc
編譯以下程式碼時:
int foo() {return 2;} int main() { foo() = 2; return 0; }
你會得到:
test.c: In function 'main': test.c:8:5: error: lvalue required as left operand of assignment
沒錯,這個例子有點誇張,不像是你能寫出來的程式碼。不過錯誤資訊中提到了左值 (lvalue)。另一個例子是當你用g++
編譯以下程式碼:
int& foo() { return 2; }
現在錯誤資訊是:
testcpp.cpp: In function 'int& foo()': testcpp.cpp:5:12: error: invalid initialization of non-const reference of type 'int&' from an rvalue of type 'int'
同樣的,錯誤資訊中提到了術語右值 (rvalue)。那麼,在 C 和 C++ 中,左值 和右值 到底是什麼意思呢?我這篇文章將會詳細解釋。
簡單的定義
這裡我故意給出了一個左值 和右值 的簡化版定義。文章剩下的部分還會進行詳細解釋。
左值 (lvalue, locator value) 表示了一個佔據記憶體中某個可識別的位置(也就是一個地址)的物件。
右值 (rvalue) 則使用排除法來定義。一個表示式不是左值 就是右值 。 那麼,右值是一個不 表示記憶體中某個可識別位置的物件的表示式。
舉例
上面的術語定義顯得有些模糊,這時候我們就需要馬上看一些例子。我們假設定義並賦值了一個整形變數:
int var; var = 4;
賦值操作需要左運算元是一個左值。var
是一個有記憶體位置的物件,因此它是左值。然而,下面的寫法則是錯的:
4 = var;// 錯誤! (var + 1) = 4; // 錯誤!
常量4
和表示式var + 1
都不是左值(也就是說,它們是右值),因為它們都是表示式的臨時結果,而沒有可識別的記憶體位置(也就是說,只存在於計算過程中的每個臨時暫存器中)。因此,賦值給它們是沒有任何語義上的意義的——我們賦值到了一個不存在的位置。
那麼,我們就能理解第一個程式碼片段中的錯誤資訊的含義了。foo
返回的是一個臨時的值。它是一個右值,賦值給它是錯誤的。因此當編譯器看到foo() = 2
時,會報錯——賦值語句的左邊應當是一個左值。
然而,給函式返回的結果賦值,不一定總是錯誤的操作。例如,C++ 的引用讓我們可以這樣寫:
int globalvar = 20; int& foo() { return globalvar; } int main() { foo() = 10; return 0; }
這裡foo
返回一個引用。引用一個左值
,因此可以賦值給它。實際上,C++ 中函式可以返回左值的功能對實現一些過載的操作符非常重要。一個常見的例子就是過載方括號操作符[]
,來實現一些查詢訪問的操作,如std::map
中的方括號:
std::map<int, float> mymap; mymap[10] = 5.6;
之所以能賦值給mymap[10]
,是因為std::map::operator[]
的過載返回的是一個可賦值的引用。
可修改的左值
左值一開始在 C 中定義為“可以出現在賦值操作左邊的值”。然而,當 ISO C 加入const
關鍵字後,這個定義便不再成立。畢竟:
const int a = 10; // 'a' 是左值 a = 10;// 但不可以賦值給它!
於是定義需要繼續精化。不是所有的左值都可以被賦值。可賦值的左值被稱為可修改左值 (modifiable lvalues) 。C99標準定義可修改左值為:
[…] 可修改左值是特殊的左值,不含有陣列型別、不完整型別、const 修飾的型別。如果它是struct
或union
,它的成員都(遞迴地)不應含有 const 修飾的型別。
左值與右值間的轉換
通常來說,計算物件的值的語言成分,都使用右值作為引數。例如,兩元加法操作符'+'
就需要兩個右值引數,並返回一個右值:
int a = 1;// a 是左值 int b = 2;// b 是左值 int c = a + b; // + 需要右值,所以 a 和 b 被轉換成右值 // + 返回右值
在例子中,a
和b
都是左值。因此,在第三行中,它們經歷了隱式的左值到右值轉換
。除了陣列、函式、不完整型別的所有左值都可以轉換為右值。
那右值能否轉換為左值呢?當然不能!根據左值的定義,這違反了左值的本質。【注:右值可以顯式地賦值給左值。之所以沒有隱式的轉換,是因為右值不能使用在左值應當出現的位置。】
不過,右值可以通過一些更顯式的方法產生左值。例如,一元解引用操作符'*'
需要一個右值引數,但返回一個左值結果。考慮這樣的程式碼:
int arr[] = {1, 2}; int* p = &arr[0]; *(p + 1) = 10;// 正確: p + 1 是右值,但 *(p + 1) 是左值
相反地,一元取地址操作符'&'
需要一個左值引數,返回一個右值:
int var = 10; int* bad_addr = &(var + 1); // 錯誤: 一元 '&' 操作符需要左值引數 int* addr = &var;// 正確: var 是左值 &var = 40;// 錯誤: 賦值操作的左運算元需要是左值
在 C++ 中'&'
符號還有另一個功能——定義引用型別。引用型別又叫做“左值引用”。因此,不能將一個右值賦值給(非常量的)左值引用:
std::string& sref = std::string();// 錯誤: 非常量的引用 'std::string&' 錯誤地使用右值 'std::string` 初始化
常量的 左值引用可以使用右值賦值。因為你無法通過常量的引用修改變數的值,也就不會出現修改了右值的情況。這也使得 C++ 中一個常見的習慣成為可能:函式的引數使用常量引用接收引數,避免建立不必要的臨時物件。
(未完待續)