左右值的概念


https://corecppil.github.io/CoreCpp2019/Presentations/Dan_Saks_Lvalues_and_Rvalues.pdf

簡述

原版PPT 有31頁,我主要摘取幾個重要的點。

下面所說的對象都是廣義的對象(object) 一個int float 都可以看作一個對象。而 類對象 與區分。

最后的總結是我自己總結的。

正文翻譯

概覽
  • 左右值並非C++ 語言特性,相反,它是表達式的屬性。
  • 本篇將會通過以下視角理解左右值:
    • 內置運算符的行為
    • 為執行操作而生成的匯編
    • 相關編譯器錯誤含義
    • 引用類型
    • 重載運算符
  • 左右值的含義已經發生變化
    • 在早期C ,左右值得概念非常簡單。
    • 早期C++,增加了類,const,引用,使得左右值概念復雜起來。
    • 現代C++增加了右值引用,使得左右值進一步復雜
  • 本文從歷史起源來解釋左右值
左值

在 The C Programming Language 中,有以下的定義:

  • 左值 起源於 賦值表達式 E1 = E2 ,其中左操作數 E1 必須為左值表達式。

  • 一個左值是一個和對象相關的表達式。而一個對象是內存的一塊區域。

    int n; //定義一個int 對象命名為 n
    n = 1; //賦值表達式
    

    在賦值表達式中:

    • n 是一個子表達式,和int 對象相關。它是左值
    • 1 是一個子表達式,但是沒有和某個對象相關。它是右值
  • 所以右值的定義就是非左值

深入底層

為什么需要區分左值和右值?

  • 編譯器可以假定右值不需要存儲空間。(這塊意思右值邏輯上不占空間,所以沒有引用數組),
  • 這在為右值表達式生成代碼提供了足夠的自由。

繼續上面的例子 n = 1

  • 如果 1 是左值的話,編譯器認為 1 和一個初始化為1 的內存中對象相關聯,假定這塊區域稱為 one

    • 當執行賦值編譯器會生成類似於 mov n, one 即將one 所代表區域的數據拷貝到 n 所代表區域。

    • 而在某些CPU 上,提供直接操作數的功能,類似於 mov n, 1 即將值1 拷貝到n 所代表的區域。

    • 在這種情況下,右值永遠不會作為一個存儲在數據空間中的對象。相對的,它作為代碼空間指令的一部分。

    • 在某些CPU 上,當將 1 賦值給對象,可能會采取這個方式: clr n inc n 即先將n 清零,再增加1 。

假設有這個例子 1 = n

  • 顯然C/C++都會報錯。但是精確原因是什么?
    • 賦值語句將一個值賦值給一個對象。
    • 左操作數必須為左值
    • 而1 是右值。

總結:所有C 中表達式 要么是左值,要么是右值。左值和數據空間中對象相關聯,右值是非左值。對於非類類型的C++ 也是正確的。但如果有類類型,就不那么准確(類對象通常不管左右值都保存在數據空間)。

其它類型

字面值:大部分字面量都是右值(1,3.2,‘c’ 等),它們不一定占用數據空間。但有些字面量(“abcdefg” 等) 卻是左值,占用數據空間。

枚舉常量:枚舉常量也是右值。

將左值用作右值

如果是非類類型,就直接將左值視為右值。如果是類類型,將會執行左值到右值的轉換。

表達式的結果

表達式例如 m + n 產生的結果存放在編譯器生成的臨時對象,通常存放在寄存器中。像這些的臨時對象都是右值。

所以 m + 1 = n 先計算 m + 1 產生右值,向右值賦值產生錯誤。

再例如 &n &1 其運算對象必須是左值,因為右值和數據空間中對象無關,不可尋址。它產生的結果卻是右值。

再次強調 左值是編譯期的屬性,如果 *p 它運算對象可以是左值,也可以是右值例如( *(p+1) ),但它返回的是左值。即使p 指向nullptr ,編譯也不會出錯,不過運行會出錯。

在C/C++ 中,非類類型的右值不會占用數據空間。而任何類型的左值都會占用。(雖然有時候編譯器會優化左值,使其不占用空間,但你應該假定左值都是占用空間)。

常對象

常對象是可尋址的對象。

編譯器可能會存儲常對象,也可能會優化掉(例如C 中常量是常變量,而 C++ 中會被編譯器替換)

C++引用類型

掌握以上左值和右值概念有助於理解C++的引用,引用使C++ 重載運算符的行為類似於內置運算符。

引用用於關聯有名對象。引用底層是常量指針,自動解引用產生左值。

enum month {
Jan, Feb, Mar, ~~~, Dec, month_end
};
typedef enum month month;
~~~
for (month m = Jan; m <= Dec; ++m) {
~~~
}

這樣的代碼在C 中可以正常工作,然而在C++中編譯錯誤。內置的++ 不能用於枚舉變量。

常量引用

常量引用可以綁定到非常量,也可以綁定到常量。而普通引用只能綁定非常量。

常量引用自動解引用產生不可改的左值。

類似於普通 指針,普通引用只能綁定到左值上。

常量引用既可以綁定到左值也可以綁定到右值。

const int &thr = 3; 在這種情況下,運行到這時,程序會創建一個int 臨時量,其值為3。然后用thr 綁定這個臨時量。當離開 thr 的范圍,程序銷毀這個臨時量。

const double &dthr = thr; 程序先會將thr 的值轉化為 double ,創建double 臨時量,然后保存轉化的值。之后用 dthr 綁定到這個臨時量。最后,離開范圍會銷毀這個臨時量。

右值引用

在C++ 03 中稱為引用的,在C++ 11 中稱為 左值引用,為了與新增的右值引用所區分。

右值引用只能綁定右值,哪怕是常右值引用。而常左值引用也可以綁定右值。

現代C++ 通常使用右值操作來避免不必要的拷貝。

與成員函數

成員函數可以用引用限定符重載,左右值調用不同版本。

總結

左右值這個概念最初是從 C 語言來的,其為了CPU 能做某些優化而設置。此時的概念是和內存中區域相關的對象都成為左值,而右值是它的補集。右值不占內存空間。右值通常都是存在 CPU 中的數據。而字面值有可能右值(‘c’),也有可能是左值(“asdfdsasd”)。

到了早期C++ (C++ 11 之前),出現了引用等。此時右值有時也占用空間(例如右值類對象)。

到了現代C++ (C++ 11及以后),出現了左值引用,右值引用的概念。為了與右值引用區分,之前的引用統稱為左值引用。右值引用的出現主要是為了避免不必要的拷貝。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM