左值和右值

来源:互联网 发布:贝爷 知乎 编辑:程序博客网 时间:2024/04/30 01:29
左值和右值


我们经常可以看到关于 左值(L-value) 和 右值(R-value) 的概念,那么到底什么是左值,什么是右值,它们之间的区别又是在哪里呢?


左值(lvalue)和右值(rvalue)最先来源于C语言。最先在C语言中表示位于赋值运算符两侧的两个值,左边的就叫左值,右边的就叫右值。
比如:
int ii = 5; //ii是左值,5是右值
int jj = ii; //jj是左值,ii是右值
上面表明,左值肯定可以作为右值使用,但反之则不然。

左值和右值的最早区别就在于能否改变。左值是可以变的,右值不能变,但是这一点在C++中已经不再成立。

在现代C++中,现在左值和右值基本上已经失去它们原本所具有的意义,对于左值表达式,通过具体名字(variant name)和引用(reference)来指定一个对象。非左值就是右值。

并且:L-value中的L指的是Location,表示可寻址。The "l" in lvalue can be though of as location.

R-value中的R指的是Read,表示可读。The "r" in rvalue can be thought of as "read" value.



我们需要理解左值和右值的定义
左值指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以做为一个左值。 右值指的是引用了一个存储在某个内存地址里的数据。

从上面的两个定义可以看出,左值其实要引用一个对象,而一个对象在我们的程序中又肯定有一个名字或者可以通过一个名字访问到,所以左值又可以归纳为:左值表示程序中必须有一个特定的名字引用到这个值。而右值引用的是地址里的内容,所以相反右值又可以归纳为:右值表示程序中没有一个特定的名字引用到这个值除了用地址。

这跟它们是否可以改变,是否在栈或堆(stack or heap)中毫无关系。


1.左值
在下面的代码中:
int ii = 5;
const int jj = ii;
int a[5];
a[0] = 100;
*(a+3) = 200;
const int& max(const int& a, const int& b) //call by reference
{
               return a > b ? a : b;
}
int& fun(int& a) //call by reference
{
               a += 5;
               return a;
}
ii,jj,a[0],*(a+3)这些值,还有函数max的返回值比如max(ii, jj),函数fun的返回值fun(ii)都是左值。因为它们都是被特定的名字所引用的值。
ii,jj,a[0],*(a+3),以及引用了max和fun的返回值的变量就是它们的名字。

注:有人会问max(8, 9)到底是左值还是右值,C++标准规定常量引用(reference to const)可以引用到右值,所以max(8, 9)似乎应该是右值。但根据我们对左值的定义,说它是左值也对。不管它是左值,还是右值,我们都不能试图去改变它。为了与前面的概念一致,姑且认为它是左值,不可改变的常量左值。
左值有不能改变的,即被const所修饰的左值,比如上面的jj,max(ii, jj),没有被const困住的左值当然是可以改变的。
比如下面的代码都是成立的:
ii = 600;
a[0] = 700;
fun(ii) = 800; //OK!
我们的眼睛没有问题,fun(ii) = 800;完全正确,因为它是可以改变的左值。



2.右值

没有特定名字的值是右值。
看下面的代码:
int fun1() //call by value
{
               …
}
int* fun2() //call by value
{
                …
}
函数fun1的返回值fun1(),函数fun2的返回值fun2()都是右值,它们的值都没有特定的名字去引用。
也许有人会奇怪,fun2()也是右值?前面说的max(a,b)不是左值吗?请看清楚,函数fun2的返回值是pointer,pointer也是call by value,而函数max的返回值是reference,reference是call by reference。当然字面上的(literal)值,比如5,8.23,’a’等等也都是右值。右值最初出现的时候,一个最大的特征就是不可改变。但时代不同了,标准也变化了。
C++中有可以改变的右值。那就是用户自定义的类(class)的构造函数生成的临时对象。至于原因,我思考了一下,我想是这样的:我们看类(class)的数据布置结构,会发现它的每一个数据成员都是有名字的,我想编译器在编译的过程中,都会生成一个外部不所知的对这个临时对象右值的名字引用。但当需要改变这个临时对象的时候,这个名字就用上了(实际这时变成了左值,因为有了名字引用)。
比如:
class Point
{
         public:
                int x;
                ……//其他各种成员函数
};
我们现在就可以改变右值,用到了匿名的引用名字。
Point().x = 6;//改变了右值


此外还要多讲一点,这点就是哪些操作符必需左值。
Operator                                                     Requirement
& (unary)                                                     Operand must be an lvalue.
++ --                                                            Operand must be an lvalue. This applies to both prefix and postfix forms.
= += -= *= %= <<= >>= &= ^= |=                   Left operand must be an lvalue.


最后说一下引用的机制。很多人都在争论引用是怎么实现的?如果你看看vc做出的反汇编代码,你会发现在机器码的层次,指针与引用的实现是相同。但是,如果你以此断定引用就是指针,那么就错了。的确,引用是靠指针实现的,但是二者有区别。我们讨论引用的问题,一定要在c++的语言层面上讨论,而不能跑到汇编代码中去看。在c++中,引用就是个别名,本身不会另外分配空间。但是在汇编层次,你会发现“引用”有自己的空间。别忘了,你已经离开了c++的范围,这样的讨论失去了意义。总之,我们应该明白,c++中的引用就是个别名,具体的实现方法也要取决于编译系统和优化方案。
原创粉丝点击