首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> 關於C++

深入理解 C/C++ 中的左值與右值

     編輯:關於C++

前言:核心概念辨析

在 C/C++ 程式設計中,「左值性」是表達式的一項基本屬性。只有透過左值表達式,我們才能存取並修改一個物件的值。雖然右值表達式在某些情況下也能引用物件並間接修改其值,但這屬於進階用法,稍後再詳述。

什麼是物件?

此處的「物件」並非狹義上的類別實例,而是指執行環境中一塊儲存區域。這塊區域的內容代表了物件的值。值得注意的是,取出物件的值需要透過特定類型來引用;同一塊儲存區域,若以不同類型解讀,可能得到不同的值,甚至導致未定義行為。

變數與物件的區別

變數是一種宣告,將名稱與物件關聯;但並非所有物件都有名稱(如暫時物件)。切勿將「暫時物件」誤稱為「暫時變數」。

一、C 語言中的左值

1.1 定義與基本性質

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 非左值

1.2 衍生左值表達式

  • *expr1(expr1 為指標)必為左值,因解參考操作引用目標物件。
  • expr1[expr2] 等價於 *(expr1 + expr2),故為左值。
  • p->member 同理為左值。
  • expr1.expr2 不一定是左值,取決於 expr1 是否為左值。

關鍵點:左值性是靜態屬性。表達式為左值不代表其引用的物件有效。例如 int *p; *p 是左值,但存取會導致未定義行為。

1.3 可修改的左值

需修改物件值的語境(賦值左操作數、自增/減操作數等)要求可修改的左值。const 修飾的左值不可修改。

const int a[2], i;
i++;        // 錯誤:i 為 const int 左值
a[0]--;     // 錯誤:a[0] 為 const int 左值

1.4 右值詳解

右值強調表達式運算後的結果(值),通常不引用物件,視為中間計算結果。即使引用物件,也無法直接修改。

1.4.1 右值的儲存位置

  • 立即數硬編碼於指令中(如 mov addr_of_i, 10)。
  • 暫存器中(如 i+j 結果暫存於 eax)。
  • 某些情況下右值也引用物件(如函數回傳結構體),但物件生存期不由程式設計師控制。

1.4.2 為何需要右值?

賦值 a=b 時,需取得 b 的值修改 a。若 b 為左值,需經「左值轉右值」轉換取出值。C++ 明確將此歸類為標準轉換之一。算術運算子要求操作數為右值,故 i+j 中 i、j 會先轉為右值。

1.4.3 右值的類型

  • C 語言:右值永遠是 cv-unqualified 類型。
  • C++:內建型別同 C;類別型別右值保留 cv-qualified 性質,因可透過成員函數間接修改物件。

1.5 運算子對左/右值的要求

  • 需值的場合(算術運算、函數參數傳值):要求右值,左值會自動轉換。
  • 需物件資訊的場合(取址、賦值左操作數、自增/減):要求左值。
  • sizeof 僅需類型資訊,不關心左/右值性。

C 標準左值表達式一覽:變數名、E[k]、(e)(e 為左值)、e.name(e 為左值)、e->name、*e、字串字面值。函數呼叫表達式永遠為右值。

1.5.1 要求左值的運算子

&(取址)、++/--、賦值左操作數。C89/90 陣列轉指標要求左值陣列;C99 移除此限制。

關於陣列類型

多數場合陣列退化為指向首元素的指標(右值)。不退化場合:宣告時、sizeof 操作數、& 取址操作數。C++ 額外增加:引用初始化、typeid/type_info 操作數、樣板推導時。

二、C++ 的左值擴展

2.1 函數左值與 register 變化

  • 函數名表達式為左值,引用函數實體;函數指標解參考亦為左值。但函數左值不可修改。
  • C++ 允許對 register 變數取址,編譯器會忽略 register 建議。

2.2 類別型別右值與暫時物件

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。

2.3 參考型別與轉換運算產生左值

  • 參考型別表達式永遠為左值(等價於其引用的物件)。
  • 轉換目標為參考型別 T& 時,轉換表達式結果為 T 類別的左值(如 dynamic_cast<base&>(d))。

2.4 成員存取表達式的左值性規則

E1.E2 為左值的條件:

  • A) E2 為參考型別 T&。
  • B) E2 為靜態成員(與 E1 左值性無關)。
  • C) E1 為左值且 E2 為資料成員。

E1->E2 視為 (*E1).E2 判斷。指向成員指標運算 E1.*E2、E1->*E2 在特定條件下亦為左值。

2.5 運算子回傳左值的差異

  • 前置 ++/--、賦值運算子回傳左值(C 中為右值)。
  • 逗號運算子:第二操作數為左值時,整體為左值。
  • 條件運算子 ? ::第二、三操作數同類型且為左值時,結果為左值。
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)。

2.6 typeid 與左值語境

  • typeid 表達式永遠為左值。
  • 需左值的場合:賦值左操作數、自增/減操作數、取址操作數(物件/函數類型)、重載運算子由函數簽名決定。

2.7 左值性對程式行為的影響

  • 需左值處無合格左值 → ill-formed;需右值處左值會經轉換(函數轉指標、陣列轉指標、左值轉右值)。
  • 需左值或僅需靜態類型資訊時(如 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(逗號運算子回傳左值)

2.8 同樣接受左右值但行為不同的場合

  • 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();  // 錯誤:同理
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved