在 C/C++ 程式設計中,「左值性」是表達式的一項基本屬性。只有透過左值表達式,我們才能存取並修改一個物件的值。雖然右值表達式在某些情況下也能引用物件並間接修改其值,但這屬於進階用法,稍後再詳述。
此處的「物件」並非狹義上的類別實例,而是指執行環境中一塊儲存區域。這塊區域的內容代表了物件的值。值得注意的是,取出物件的值需要透過特定類型來引用;同一塊儲存區域,若以不同類型解讀,可能得到不同的值,甚至導致未定義行為。
變數是一種宣告,將名稱與物件關聯;但並非所有物件都有名稱(如暫時物件)。切勿將「暫時物件」誤稱為「暫時變數」。
C 標準定義:左值是指向物件的表達式,可透過它取得物件的值。若為可修改的左值,還能修改該物件的值。C++ 擴展了此概念,允許左值引用函數。
因左值指向物件,故可對其取址(&運算)。但有兩類左值不可取址:位域成員(最小定址單位為 byte)與 register 修飾的變數(編譯器可能優化至暫存器)。
char a[10]; // a 是代表 int[10] 的左值
char (*p)[10] = &a; // &a 取得陣列位址
const char* p = "hello world"; // 字串字面值在 C 中為 char[12] 左值
char (*p)[12] = &"hello world";
struct S { int a:2; int b:8; };
struct S t;
int* p = &t.a; // 錯誤:位域不可取址
register int i;
int* p = &i; // 錯誤:register 變數不可取址
int a, b;
int* p = &(a+b); // 錯誤:a+b 非左值
*expr1(expr1 為指標)必為左值,因解參考操作引用目標物件。expr1[expr2] 等價於 *(expr1 + expr2),故為左值。p->member 同理為左值。expr1.expr2 不一定是左值,取決於 expr1 是否為左值。關鍵點:左值性是靜態屬性。表達式為左值不代表其引用的物件有效。例如 int *p; *p 是左值,但存取會導致未定義行為。
需修改物件值的語境(賦值左操作數、自增/減操作數等)要求可修改的左值。const 修飾的左值不可修改。
const int a[2], i;
i++; // 錯誤:i 為 const int 左值
a[0]--; // 錯誤:a[0] 為 const int 左值
右值強調表達式運算後的結果(值),通常不引用物件,視為中間計算結果。即使引用物件,也無法直接修改。
mov addr_of_i, 10)。i+j 結果暫存於 eax)。賦值 a=b 時,需取得 b 的值修改 a。若 b 為左值,需經「左值轉右值」轉換取出值。C++ 明確將此歸類為標準轉換之一。算術運算子要求操作數為右值,故 i+j 中 i、j 會先轉為右值。
sizeof 僅需類型資訊,不關心左/右值性。C 標準左值表達式一覽:變數名、E[k]、(e)(e 為左值)、e.name(e 為左值)、e->name、*e、字串字面值。函數呼叫表達式永遠為右值。
&(取址)、++/--、賦值左操作數。C89/90 陣列轉指標要求左值陣列;C99 移除此限制。
多數場合陣列退化為指向首元素的指標(右值)。不退化場合:宣告時、sizeof 操作數、& 取址操作數。C++ 額外增加:引用初始化、typeid/type_info 操作數、樣板推導時。
register 變數取址,編譯器會忽略 register 建議。C++ 類別型別右值引用暫時物件,可透過非 const 成員函數間接修改該物件。
class A { int i; public: void f() { i=0; } void g() const { } };
A foo1(); const A foo2();
A().f(); // OK:修改暫時物件
A().g(); // OK
foo1().f(); // OK
foo1().g(); // OK
typedef const A B;
B().f(); // 錯誤:const 右值無法呼叫非 const 成員函數
B().g(); // OK
foo2().f(); // 錯誤
foo2().g(); // OK
C++ 類別型別右值保留 cv-qualified,C 則忽略 const。
T& 時,轉換表達式結果為 T 類別的左值(如 dynamic_cast<base&>(d))。E1.E2 為左值的條件:
T&。E1->E2 視為 (*E1).E2 判斷。指向成員指標運算 E1.*E2、E1->*E2 在特定條件下亦為左值。
? ::第二、三操作數同類型且為左值時,結果為左值。int i, j; int &ri = i;
int* pi = &(i=1); // OK:i=1 為可修改左值
++++i; // OK:但可能因序列點規則導致 UB
(i=2, j) = 1; // OK:逗號運算子回傳 j(左值)
((i==1) ? ri : j) = 0; // OK:條件運算子回傳左值
警示:C 中 well-formed 代碼在 C++ 可能產生 UB,或雙方行為不同。如 i=j=1 在 C 合法,C++ 可能 UB;sizeof(0, array) C 中等同 sizeof(char*),C++ 中等同 sizeof(array) (100)。
typeid 與左值語境typeid 表達式永遠為左值。sizeof、參考繫結),會抑制標準轉換。char c[100];
char (&rarr)[100] = c; // 抑制陣列轉指標
char* p1 = c; // 發生陣列轉指標
void f();
void (&rf)() = f; // 抑制函數轉指標
void (*pf)() = f; // 發生函數轉指標
sizeof(0, c); // C: sizeof(char*);C++: 100(逗號運算子回傳左值)
typeid:多型類型左值觸發執行期檢查(RTTI),右值或非多型類型靜態綁定。class B { virtual f() {} }; class A {};
B& foo();
typeid(foo()); // 執行期檢查
typeid(A()); // 靜態綁定
typeid(B()); // 靜態綁定
class C { private: C(const C&); };
extern C c; C g();
C& rc1 = c; // OK:左值直接繫結
C& rc2 = g(); // 錯誤:右值需暫時物件,但複製建構函數 private
const C& r2 = g(); // 錯誤:同理