前言
爲什麼標題要寫成簡單聊聊,而不是寫成什麼“C++中左值與右值詳解”或者現在很流行的“驚了!看了這一篇左值與右值講解,他吊打了面試官”,其實帶有詳解這個詞是需要勇氣的,最起碼要融會貫通之後纔敢這麼說吧,本來是學習右值引用的,結果涉及到了左值和右值,然後去了解他們歷史發現也是有些混亂,操作中又經常涉及到運算符優先級,真是越學越亂了。
問題
索性也把右值引用放一邊,從頭來看看這個左值和右值,其實我跟這兩個詞一點都不熟,最多就是在編譯報錯的提示框中看到他們,當然有時候也會看到他們的英文名字 lvalue
和 rvalue
,這時候一般就是編譯器開始抱怨了,說我寫了什麼它不能理解的東西,其實嘛,我自己都沒完全理解,從現在開始邊學邊總結了,先展示一個常見報錯:
error: lvalue required as increment operand
這是什麼意思,這麼繞嘴,左值需要作爲增長操作數,請說人話:自增操作需要一個可以賦值的變量作爲操作數,需要變量就直說嘛,爲什麼要左值、右值的把人都繞蒙了。
歷史淵源
這個世界一直是在變化的,可能之前你一直引以爲豪的經驗大樓,轉眼之間就會傾塌。關於左值和右值的歷史,普遍的觀點是最初來源於 C
語言,後來被引入到了 C++
,但是關於左值和右值的含義和實現卻在一直改變和完善,對於它的歷史講解發現一篇總結的比較好的文章 《C/C++ 左值和右值, L-value和R-value》。
這是2012年的一篇文章,文中給出了歷史說明依據,最後還舉了一些例子來說明 C
和 C++
關於左值實現的不同,但是實際操作後你會發現,時間的車輪早已向前行進了一大截,文中提到的那些不同,在最新的 gcc
和 g++
編譯器上早已變得相同,文中提到的反例現在看來幾乎沒有意義了。
簡單梳理下,左值的定義最早出現在 《The C Programming Language 》一書中,指的是引用一個對象,放在賦值表達式 =
左邊的值。
後來在新的 C
語言標準中提到左值是賦值表達式 =
左邊的值或者需要被改變的值,而等號的右邊的值被稱爲右值。左值更好的表達爲可以定位的值,而右值是一種表達數據的值,基於這個表述 L-value
可以理解爲 locator value
,代表可尋址,而 R-value
可以理解爲 read value
,代表可讀取。
不過以上的新解,完全是人們爲了理解左值、右值賦予的新含義,從歷史發展來看,一開始左值和右值完全就是通過等號的左邊和右邊來命名的,只不過隨着標準的完善和語言的發展、更替,雖然兩個名字保留了下來,但是它們的含義卻在逐步發生改變,與最初誕生時的 =
左右兩邊的值這個含義相比,已經相差很多了。
認識左值和右值
關於左值右值有幾條規則和特點,先列舉在這裏,後面可以跟隨例子慢慢體會:
- 左值和右值都是指的表達式,比如
int a = 1
中的a
是左值,++a
是左值,func()
也可能是左值,而a+1
是右值,110
也是一個右值。 - 左值可以放在
=
的左邊,右值只能放在=
的右邊,這其中隱含的意思就是左值也能放在=
的右邊,但是右值不能放在=
的左邊。 - 左值可以取地址,代表着內存中某個位置,可以存儲數據,右值僅僅是一個值,不能取地址,或者它看起來是一個變量,但它是臨時的無法取地址,例如一個函數的非引用的值返回。
以上規則從定義來看一點也不嚴謹,比如一個常量定義是可以賦值,後面就不行了,它也可以取地址,但是不能賦值的它到底是左值還是右值,這點其實不用糾結,心裏知道這個情況就可以了。
再比如一個普通變量,它原本是一個左值,當用它給其他變量賦值的時候,它又化身爲一個右值,這時它也可以取地址,好像與上面的說法相違背了,但是仔細想想真的是這樣嗎?它只是臨時化身爲右值,其實是一個左值,所以纔可以取地址的。
其實你如果不做學術研究、不斤斤計較,那麼完全可以把能夠賦值的表達式作爲左值,然後把左值以外的表達式看成右值,如果你不熟悉解左值和右值可能根本不會影響你平時的工作和學習,但是瞭解它有助於我們深入理解一些內置運算符和程序執行過程,以及在出現編譯錯誤的時候及時定位問題。
具體的示例
最簡單的賦值語句
int age = 18;
這個賦值語句很簡單,=
作爲分界線,左邊的 age
是左值,可以被賦值,可以取地址,它其實就是一個表達式,代表一個可以存儲整數的內存地址;右邊的 18
也是一個表達式,明顯只能作爲右值,不能取地址。
18 = age;
這個語句在編譯時會提示下面的錯誤:
error: lvalue required as left operand of assignment
錯誤提示顯示:賦值語句的左邊需要一個左值,顯然 18
不能作爲左值,它不代表任何內存地址,不能被改變。
如果程序中的表達式都這麼簡單就不需要糾結了,接着我們往下看一些複雜點的例子。
自增自減運算
++age++;
第一眼看到這個表達式,你感覺它會怎樣運算,編譯一下,你會發現編譯失敗了,錯誤如下:
error: lvalue required as increment operand
加個括號試試:
++(age++)
編譯之後會出現相同的錯誤:
error: lvalue required as increment operand
再換一種加括號的方式再編譯一次:
(++age)++
這次成功編譯了,並且輸出值之後發現 age
變量增加了兩次。
先不考慮左值右值的問題,我們可以從這個例子中發現自增運算的優先級,後置自增 age++
的優先級要高於前置自增 ++age
的優先級。
現在回過頭來看看之前的編譯錯誤,爲什麼我們加括號改變運算順序之後就可以正常執行了呢?這其實和自增運算的實現有關。
前置自增
前置自增的一般實現,是直接修改原對象,在原對象上實現自增,然後將原對象以引用方式返回:
UPInt& UPInt::operator++()
{
*this += 1; // 原對象自增
return *this; // 返回原對象
}
這裏一直操作的是原對象,返回的也是原對象的引用,所以前置自增表達式的結果是左值,它引用的是原對象之前所佔用的內存。
後置自增
後置自增的一般實現,是先將原對象的數據存儲到臨時變量中,接着在原對象上實現自增,然後將臨時變量以只讀的方式返回:
const UPInt UPInt::operator++(int)
{
UPInt oldValue = *this; // 將原對象賦值給臨時變量
++(*this); // 原對象自增
return oldValue; // 返回臨時變量
}
這裏返回的是臨時變量,在函數返回後就被銷燬了,無法對其取地址,所以後置自增表達式的結果是右值,不能對其進行賦值。
所以表達式 ++age++;
先進行後置自增,然後再進行前置自增就報出編譯錯誤了,因爲不能修改右值,也不能對右值進行自增操作。
自增表達式賦值
前面說到前置自增表達式是一個左值,那能不能對其賦值呢?當然可以!試試下面的語句:
++age = 20;
這條語句是可以正常通過編譯的,並且執行之後 age
變量的值爲 20
。
函數表達式
函數可以作爲左值嗎?帶着這個疑問我們看一下這個賦值語句:
func() = 6;
可能有些同學會有疑問,這是正常的語句嗎?其實它是可以正常的,只要 func()
是一個左值就可以,怎麼才能讓他成爲一個左值呢,想想剛纔的前置自增運算可能會給你啓發,要想讓他成爲左值,它必須代表一個內存地址,寫成下面這樣就可以了。
int g;
int& func()
{
return g;
}
int main()
{
func() = 100;
}
函數 func()
返回的是全局變量 g
的引用,變量 g
是一個可取地址的左值,所以 func()
表達式也是一個左值,對其賦值後就改變了全局變量 g
的值。
那麼我們注意到這裏 func()
函數返回的是全局變量的引用,如果是局部變量會怎麼樣呢?
int& func()
{
int i = 101;
return i;
}
int main()
{
func() = 100;
}
上面的代碼編譯沒有錯誤,但是會產生一個警告,提示返回了局部變量的引用:
warning: reference to local variable ‘i’ returned [-Wreturn-local-addr]
運行之後可就慘了,直接顯示段錯誤:
Segmentation fault (core dumped)
改爲局部變量之後,func()
函數雖然返回了一個值,但是這個值是一個臨時值,函數返回之後該值被銷燬,對應的內存空間也不屬於它了,所以在最後賦值的時候纔會出現段錯誤,就和我們訪問非法內存是產生的錯誤時一樣的。
總結
- 可以被賦值的表達式是左值,左值可以取地址。
- 右值應該是一個表示值的表達式,不是左值的表達式都可以看成是右值
- 後置自增操作符的優先級要高於前置自增操作符,它們是按照從右向左結合的
- 關於左值和右值的知識點還有很多,後續想到了再補充,我也是邊學邊總結,如果有錯誤也歡迎小夥伴們及時指出,我會及時改正的
時刻靜下來想想當初爲什麼出發,不要在現實的汪洋中偏離航向