当前位置:天才代写 > tutorial > C语言/C++ 教程 > C++中关于左值和右值的接头

C++中关于左值和右值的接头

2017-11-05 08:00 星期日 所属: C语言/C++ 教程 浏览:682

副标题#e#

左值性(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.


#p#副标题#e#

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];  // (*)
}

#p#分页标题#e#

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.
}

#p#副标题#e#

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(字符串字面值) |     \            |

————————————————–

#p#分页标题#e#

这里的左值表达式在前面有得已经说明。只说明一下其余的几个表达式,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的操纵数和模板推导时。

#p#副标题#e#

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

#p#分页标题#e#

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中函数挪用的返回值老是右值的。

#p#副标题#e#

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是一个指向数据成员的指针的条件下,该表达式是左值。

#p#副标题#e#

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.

#p#分页标题#e#

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++.

#p#副标题#e#

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)又不行会见,因此以上声明语句犯科。

 

    关键字:

天才代写-代写联系方式