c/c++ 左值 右值討論

來源:互聯網
上載者:User
左值性(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)又不可訪問,因此以上聲明語句非法。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.