程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++中關於左值和右值的討論

C++中關於左值和右值的討論

編輯:關於C++

左值性(lvalueness)在C/C++中是表達式的一個重要屬性。只有通過一個左值表達式才能來引用及更改一個對象(object)的值。(某些情況下,右值表達式也能引用(refer)到某一個對象,並且可能間接修改該對象的值,後述)。

何謂對象?如果沒有明確說明,這裡說的對象,和狹義的類/對象(class/object)相比,更為廣泛。在C/C++中,所謂的對象指的是執行環境中一塊存儲區域(a region of storage),該存儲區域中的內容則代表(represent)了該對象的值(value)。注意到我們這裡所說的"代表",對於一個對象,如果我們需要取出(fetch)它的值,那麼我們需要通過一定的類型(type)來引用它。使用不同的類型,對同一對象中的內容的解釋會導致可能得到不同的值,或者產生某些未定義的行為。

在介紹左值之前,我們還需要引入一個概念: 變量(variable)。經常有人會把變量與對象二者混淆。什麼叫變量?所謂變量是一種聲明,通過聲明,我們把一個名字(name)與一個對象對應起來,當我們使用該名字時,就表示了我們對該對象進行某種操作。但是並不是每個對象都有名字,也並不意味著有對應的變量。比如臨時對象(temporary object)就沒有一個名字與之關聯(不要誤稱為臨時變量,這是不正確的說法)。

1 C中的左值

1.1按照C的定義,左值是一個引用到對象的表達式,通過左值我們可以取出該對象的值。通過可修改的左值表達式(modifiable lvalue)我們還可以修改該對象的值。(需要說明的是,在C++中,左值還可以引用到函數,即表達式f如果引用的是函數類型,那麼在C中它既不是左值也不是右值;而在C++中則是左值)。因為左值引用到某一對象,因此我們使用&對左值表達式(也只能對左值表達式和函數)取址運算時,可以獲得該對象的地址(有兩種左值表達式不能取址,一是具有位域( bit-field )類型,因為實現中最小尋址單位是 byte;另一個是具有register指定符,使用register修飾的變量編譯器可能會優化到寄存器中)。

Ex1.1
char a[10];  // a is an lvalue representing an array of 10 ints.
char (* p)[10]=&a; // &a is the address of the array a.
const char* p="hello world"; //"hello world" is an lvalue of type char[12]
//in C,  type const char[12] in C++.
char (*p)[12]=&"hello world";

struct S{ int a:2; int b: 8; };
struct S  t;
int* p=&t.a; //error. t.a is an lvalue of bitfield.

register int i;
int * p=&i; //error. i is an lvalue of register type.
int a, b;
int * p=& (a+b); //error. a+b is not an lvalue.

1.2假設expr1是一個指向某對象類型或未完整類型(incomplete type,即該類型的布局和大小未知)的指針,那麼我們可以斷言*expr1一定是個左值表達式,因為按照*運算符的定義,*expr1表示引用到expr1所指向的對象。如果expr1是個簡單的名字,該名字代表一個變量。

同樣的,該表達式也是個左值,因為他代表的是該變量對應的對象。對於下標運算符,我們一樣可以做出同樣的結論,因為expr1[expr2]總是恆等於*( ( expr1 )+ expr2 ),那麼p->member,同樣也是一個左值表達式。然而對於expr1.expr2,則我們不能斷定就是個左值表達式。因為expr1可能不是左值。

需要特別說明的是,左值性只是表達式的靜態屬性,當我們說一個表達式是左值的時候,並不意味著它一定引用到某一個有效存在的對象。int *p; *p是左值表達式,然而這裡對*p所引用的對象進行讀寫的結果將可能是未定義的行為。

Ex1.2
extern struct A a;
struct A* p2= &a;

a是個左值表達式,因而可以進行&運算,然而此時stru A仍然沒有完整。

//In C++
extern class A a;
A & r=a;// OK. Refers to a, though a  with an incomplete type.

1.3可修改的左值

在語義上需要修改左值對應的對象的表達式中,左值必須是一個可修改的左值。比如賦值(包括復合賦值)表達式中的左操作數,必須是一個可修改的左值表達式;自增/減運算符的操作數等。

Ex1.3
const int a[2], i; //NOTE: a unintialized. legal in C, illegal in C++.
i++; //error, i is an lvalue of type const int.
a[0]--;//error, a[0] is an lvalue of const int.

1.4 右值與左值相對應的另一個概念是右值(rvalue)。在C中,右值也用表達式的值(value of the expression)來表達。即右值強調的不是表達式本身,而是該表達式運算後的結果。這個結果往往並不引用到某一對象,可以看成計算的中間結果;當然它也可能引用到某一對象,但是通過該右值表達式我們不能直接修改該對象。

1.4.1 右值的存儲位置

Ex1.4

int i;

i=10;

10在這裡是一個右值表達式,上句執行的語義是用整型常量10的值修改i所引用的對象。

從匯編語言上看,上述語句可能被翻譯成:

mov addr_of_i,10;

10這個值被硬編碼到機器指令中;

右值也可以存儲在寄存器中:

int i,j,k;

k=i+j;

i+j表達式是個右值,該右值可能存儲在寄存器中。

mov eax, dword ptr[addr_of_i];
mov ebx, dword ptr[addr_of_j];
add eax, ebx;
mov dword ptr[addr_of_k], eax;

在這裡,i+j表達式的結果在eax中,這個結果就是i+j表達式的值,它並不引用到某一對象

某些情況下,一個右值表達式可能也引用到一個對象。

struct S{ char c[2];};
struct S f(void);

void g()
{
f().i;
f().c[1];  // (*)
}

f()表達式是個函數調用,該表達式的類型是f的返回類型struct S,f()表達式為右值表達式,但是在這裡往往對應著一個對象,因為這裡函數的返回值是一個結構,如果不對應著一個對象(一片存儲區域),用寄存器幾乎不能勝任,而且[]操作符語義上又要求一定引用到對象。

右值雖然可能引用到對象,然而需要說明的是,在右值表達式中,是否引用到對象及引用得對象的生存期往往並不是程序員所能控制。

1.4.2為什麼需要右值?右值表示一個表達式運算後的值,這個值存儲的地方並沒有指定;當我們需要一個表達式運算後的值時,即我們需要右值。比如在賦值運算時,a=b;我們需要用表達式b的值,來修改a所代表的對象。如果b是個左值表達式,那麼我們必須從b所代表的對象中取出(fetch)該對象的值,然後利用該值來修改a代表的對象。這個取出過程,實際上就是一個由左值轉換到右值的過程。這個過程,C中沒有明確表述;但在C++中,被明確歸納為標准轉換之一,左值到右值轉換(lvalue-to-rvalue conversion)。回頭看看上面的代碼,i+j表達式中,+運算符要求其左右操作數都是右值。行1和2,就是取出左值表達式i,j的對應的對象的值的過程。這個過程,就是lvalue-to-rvalue conversion.i+j本身就是右值,這裡不需要執行lvalue-to-rvalue conversion,直接將該右值賦值給k.

1.4.3右值的類型右值表達式的類型是什麼? 在C中,右值總是cv-unqualified的類型。因為我們對於右值,即使其對應著某個對象, 我們也無從或不允許修改它。而在C++中,對於built-in類型的右值,一樣是cv-unqualified,但是類類型(class type)的右值,因為C++允許間接修改其對應的對象,因此右值表達式與左值一樣同樣有cv-qualified的性質。(詳細見後)

Ex1.5
void f(int);
void g()
{
const int i;
f(i); //OK. i is an lvalue.After an lvalue-to-rvalue conversion, the rvalue's
//type is int.
}

1.5 在理解了左值和右值的概念後,我們就能夠更好理解為什麼有些運算符需要右值,而某些場合則需要左值。簡單說來,當操作符的某個操作數是一個表達式,而我們只需要該表達式的值時,如果該表達式不是右值,那麼我們需要取出該表達式的值;比如算術運算中,我們只需要左右操作數的值,這個時候對左右操作數的要求就是右值。任何用作其操作數的左值表達式,都需要轉化為右值。如果我們需要的不是該表達式的值,而是需要使用表達式的其他信息;比如我們只是關心表達式的類型信息,比如作為sizeof的操作數,那麼我們對表達式究竟是左值還是右值並不關心,我們只需要得到他的類型信息。(按,在C++中,表達式的類型信息也有靜態、動態之分,這個情況下,表達式的左值性,也會影響到一些操作符的語義。比如typeid,後有分析)。有些操作符則必需要求其操作數是左值,因為他們需要的是該表達式所引用對象的信息,比如地址;或者希望引用或修改該對象的值,比如賦值運算。

根據分析,我們可以總結出哪些表達式在C中是左值,而哪些操作符又要求其操作數是左值表達式:

表1:左值表達式  (From C  Reference Manual  )

--------------------------------------------------

表達式              |            條件            |

__________________________________________________

Name         |        Name 為變量名       |

--------------------------------------------------

E[k]           |            \               |

--------------------------------------------------

(e) //括號表達式    |      e 為左值              |

--------------------------------------------------

e.name          |      e 為左值              |

--------------------------------------------------

e->name         |         \                  |

--------------------------------------------------

*e            |         \                  |

--------------------------------------------------

string literal(字符串字面值) |     \            |

--------------------------------------------------

這裡的左值表達式在前面有得已經說明。只說明一下其余的幾個表達式,e.name,如果e是左值,e.name表示引用對象e中的成員name,因而也是左值;括號表達式不改變e的意義,因而不影響他的左值性。另外函數調用(function call)表達式總是右值的。需要特別強調的是string literal,前面已經說明它是左值,在C中具有類型char [N],而在C++中類型則為const char[N]. C中之所以為char [N]類型,主要是保持向前兼容。C++中的左值表達式要更為復雜,但是C中左值表達式,在C++中依然是左值的。

1.5.1要求操作數為左值的操作符有:&(取址運算)(其操作數也可以是函數類型);++/——:賦值表達式的左操作數。另外還有一點需要提及的,在數組到指針轉換(array-to-pointer conversion )中, C89/90中要求數組必須是左值數組才能執行該轉換。

Ex1.6
char c[2];
c[0]; //

c[0]相當於*(?+0); 然後表達式c從char[2]類型的左值轉換為一個char*的右值,該右值代表了數組首元素的地址;

Ex1.7

struct S{ char c[2]; } f(void);
void g()
{
f().c[0];
f().c[0]=1; (*)
}

表達式f()。c[0]相當於*( (f()。c)+0 ),然而在這裡f()。c是一個右值數組,在C89/90中,因此上述表達式是非法的;而在C99中,array to pointer conversion已經不要求是左值數組,上述表達式合法。另外,在這裡f()雖然是右值,但是f()卻引用到一個對象,我們通過f()。c[0]左值表達式可以引用到該對象的一部分,並且通過(*)可以修改它(因為該左值表達式是modifiable lvalue,但是嘗試修改它的行為是未定義的,然而從左右值性上是行得通的 )

* 關於數組類型

數組類型在大部分場合都會退化為一個指向其首元素的指針(右值),因而在左右值性和類型判斷上,容易發生誤解。數組不發生退化的地方有幾個場合,一是聲明時;一是用作sizeof的操作數時;一是用作&取址運算的操作數時。

Ex1.8

int a[3]={1,2,3};
int b[3];
b=a; //error. b converted to int* (rvalue): array degration.
int* p=a; //array degration: a converted to an rvalue of int*
sizeof(a); // no degration.
&a; //no degration.

C++中,數組還有其他場合不發生退化,比如作為引用的initializer;作為typeid/ typeinfo的操作數和模板推導時。

2 C++的左值

2.1

與C不同的是,C++中,一個左值表達式,不僅可以引用到對象,同樣也可以引用到一個函數上。假定f是一個函數名,那麼表達式f就表示一個左值表達式,該表達式引用到對應的函數;void (*p)(void); 那麼*p也是一個左值。然而一個函數類型的左值是不可修改的。 *p=f;// error. (注:類的non-static member function,不是這裡指的函數類型,它不能脫離對象而存在; 而static member function是函數類型)

另一個不同之處在於,對於register變量,C++允許對其取址,即register int i; &i;這種取址運算,事實上要求C++編譯器忽略掉register specifier指定的建議。無論在C/C++中, register與inline一樣都只是對編譯器優化的一個建議,這種建議的取捨則由編譯器決定。

C++中,一個右值的class type表達式,引用到一個對象;這個對象往往是一個臨時對象(temporary object)。在C++中,我們可以通過class type的右值表達式的成員函數來間接修改對象。

Ex2.1
class A
{
int i,j;
public:
void f(){ i=0; }
void g() const { i; }
};

A foo1();
const A foo2();

A().f(); //ok, modify the temporary object.
A().g();//ok, refer to the temporary object.

foo1().f();//ok, modify the temporary object.
foo1().g();//ok, refer to the temporary object.

typedef const A B;
B().f(); //error. B()' s an rvalue with const A,
B().g(); //ok, refer to the temporary object.

foo2().f();//error. B()' s an rvalue with const A,
foo2().g();//ok, refer to the temporary object

需要再次說明的是,C++中的class type的右值表達式可以具有const/volatile屬性,而在C中右值總是cv-unqualified.

Ex2.2
struct A{ char c[2]; };
const struct A f();

在C中,f()的返回值總是一個右值表達式,具有struct A類型(const被忽略)。

2.2

C++中引入了引用類型(reference type),引用總是引用到某一對象或者函數上,因此當我們使用引用時,相當於對其引用的對象/函數進行操作,因而引用類型的表達式總是左值。

(在分析表達式類型時,如果一個表達式expr最初具有T&類型,該表達式會被看作具有類型T的左值表達式)

Ex2.3
extern int& ri;
int & f();

int g();
f()=1;
ri=1;
g()=1;// error.

函數調用f()的返回類型為int&, 因此表達式f()的類型等價於一個int類型的左值表達式。

而函數調用g()的返回類型為int,因此表達式g()為int類型的右值表達式。

與C++相比,C中函數調用的返回值總是右值的。

2.3 與C相比,在C++中,轉換表達式可能產生左值。如果轉換的目標類型為引用類型T&,轉換表達式的結果就是一個類型T的左值表達式。

Ex2.4
struct base{ //polymorphic type
int i;
virtual f() { }// do nothing;
};

struct derived: base {
};

derived d;
base b;
dynamic_cast<base&>(d).i=1; // dynamic_cast yields an lvalue of type base.
dynamic_cast<base>(d); //yields an rvalue of type base.
static_cast<base&>(d).i=1; //

( (base&)d ).i=1;

2.4

對於member selection表達式E1.E2,C++則比C要復雜的多。

A)如果E2表達式類型是T&,那麼表達式E1.E2也是一個T的左值表達式。

B)如果E2不是引用類型的話,但是一個類型T的static 成員(data/function member),那麼E1.E2也將是一個T的左值表達式,因為E1.E2實際上引用到的是一個static成員,該成員的存在與否與E1的左值性沒有關系。

C)如果E1是左值,且E2是數據成員(data member),那麼E1.E2 是左值,引用到E1所代表的對象的相應的數據成員。

Ex2.5

struct A{
public:

static int si;
static void sf();

int i;
void f();
int & ri;
};

extern A a;

A g();

void test()
{
void (*pf1)()=&a.sf; //a.sf is an lvalue of function type, refers to A::sf.
void (*pf2)()=&a.f; //error. a.f is not an lvalue and types mismatch.

g().ri=1; //ok. g().ri is a modifiable lvalue, though g() is an rvalue.
g().si=1; //ok. Refers to A::si;
g().i=1; //error. g().i is an rvalue.

a.i=1; //ok. a is an lvalue.

}

對於E1->E2,則可以看成(*E1)。E2再用上述方式來判斷。

對於表達式E1.*E2, 在E1是左值和E2是一個指向數據成員的指針的條件下,該表達式是左值。

對於E1->*E2,可以看作(*E1)。*E2.在E2是一個指向數據成員的指針的條件下,該表達式是左值。

2.5 與C相比,C++中前綴++/——表達式、賦值表達式都返回左值。

逗號表達式的第二個操作數如果是左值表達式的話,逗號表達式也是左值表達式。

條件表達式(? :)中,如果第2和第3個操作數具有相同類型,那麼該條件表達式的結果也是左值的。

Ex2.6

int i,j;
int & ri=i;
int* pi=&(i=1); //ok, i=1 is an modifiable lvalue referring to i;
++++i; //ok. But maybe issues undefined behavior because of the constraints
//about sequence points.

(i=2, j) =1; //ok
( ( I==1 ) ? ri : j ) = 0; //ok

需要當心的是,因為這種修改,在C中well-formed的代碼,在C++中可能會產生未定義行為;另一方面會造成在C/C++中即使都是well-formed的代碼,也會產生不同的行為。

Ex2.7
int i, j;
i=j=1; //well formed in C, maybe undefined in C++.

char array[100];
sizeof(0, array);

在C中,sizeof(0, array) == sizeof(char*)。而在C++中,sizeof(0, array)== 100;

2.6

typeid表達式總是左值的。

2.7

C++中需要左值的場合:

1) 賦值表達式中的左操作數,需要可修改的左值;

2) 自增/減中的操作數;

3) 取址運算中的操作數,需要左值(對象類型,或者函數類型),或者qualified id.

4) 對於重載的操作符來說,因為執行的是函數的語義,該操作符對於操作數的左值性要求、該操作符表達式的類型和左值性, 均由其函數聲明決定。

2.8

左值性對程序行為的影響:

2.8.1

表達式的左值性對於程序的行為有重要的影響。如果一個需要(可修改)左值的場合,而沒有對應的符合要求的左值,程序將是ill-formed;反之,如果需要一個右值的話,那麼一個左值表達式就需要通過lvalue-to-rvalue conversion,轉換為一個右值。

一個需要左值,或僅僅需要表達式的靜態類型信息,而不關心表達式的左值性的場合,則往往抑制了在左值表達式上的一些標准轉換;而對於需要右值的場合,一個左值表達式往往需要經過函數-指針轉換、數組-指針轉換和左右值轉換等。

Ex2.8
char c[100];
char (&rarr)[100]=c; // suppresses array-to-pointer conversion on c.
char* p1=c; //c will be adjusted to type "char *".
void f();
void (&rf)()=f; //suppresses function-to-pointer conversion on expression f
void (*pf)()=f; //f will be adjusted to type "pointer to function
//type void () ".
sizeof(0, c); // the expression's value is sizeof(char*) in c;
//100 in C++, since the expression (0,c) is an lvalue
//in C++.

2.8.2 除此之外,有些場合無論左右值都可接受,但是根據表達式的左右值性,執行不同的行為:

Ex2.9

typeid's behavior

class B{ public: virtual f() { } }; //polymorphic type.
class A {};
B& foo();

typeid(foo()); // foo() is an lvalue of a polymorphic type. Runtime check.
typeid(A()); //rvalue, statically
typeid(B()); //rvalue, statically

A a;
typeid(a); //lvalue of a non-polymorphic type, statically.

//..........................
reference's behavior

class C{ private: C(const C&); };//NOTICE

extern C c;
C g();

C& rc1=c; //bind directly. (1)
C& rc2=g()//error. (2)
const C& r2=g(); //error. (3)

這裡表達式c是個左值表達式,且其類型(C)與rc1的類型兼容,因此(1)式直接綁定;而(2)和(3)中,右邊的表達式均為右值表達式,且該右值表達式又不能通過user conversion轉換成與左邊的變量兼容的類型,因此語義上這裡需要構造一個臨時對象,並使用C的構造函數和右值g()來初始化它,但是這裡的C的拷貝構造函數(copy ctor)又不可訪問,因此以上聲明語句非法。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved