左值性(lvalueness)在C/C++中是運算式的一個重要屬性。只有通過一個左值運算式才能來引用及更改一個對象(object)的值。(某些情況下,右值運算式也能引用(refer)到某一個對象,並且可能間接修改該對象的值,後述)。
何謂對象?如果沒有明確說明,這裡說的對象,和狹義的類/對象(class/object)相比,更為廣泛。在C/C++中,所謂的對象指的是執行環境中一Block Storage地區(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)又不可訪問,因此以上聲明語句非法。