概念
C语言中的表达式被分为左值和其它(函数和非对象值),其中左值被定义为标识一个对象的表达式。在C语言中lvalue
是locator value
的简写,因此lvalue
对应了一块内存地址。
C++11之前,左值遵循了C语言的分类法,但与C不同的是,其将非左值表达式统称为右值,函数为左值,并添加了引用能绑定到左值但唯有const
的引用能绑定到右值的规则。几种非左值的C表达式在C++中成为了左值表达式。
自C++11开始,对值类别又进行了详细分类,在原有左值的基础上增加了纯右值和消亡值,并对以上三种类型通过是否具名(identity)和可移动(moveable),又增加了glvalue
和rvalue
两种组合类型。
- 具名(identity):可以确定表达式是否与另一表达式指代同一实体,例如通过比较它们所标识的对象或函数的(直接或间接获得的)地址
- 可被移动(moveable):移动构造函数、移动赋值运算符或实现了移动语义的其他函数重载能够绑定于这个表达式
表达式是可求值的,对表达式求值可得到一个结果,这个结果有两个属性
- 类型,比如
int
、string
、引用或者我们自定义的类 - 值类别
- 在C++11之前,表达式的值分为左值和右值两种,其中右值就是我们理解中的字面值
1
、true
、nullptr
等 - 自C++11开始,表达式的值分为左值(
lvalue
, left value)、将亡值(xvalue
, expiring value)、纯右值(pvalue
, pure ravlue)以及两种混合类别泛左值(glvalue
, generalized lvalue)和右值(rvalue
, right value)五种
- 在C++11之前,表达式的值分为左值和右值两种,其中右值就是我们理解中的字面值
值类别
- 左值(lvalue):具名且不可被移动
- 将亡值(xvaue):具名且可被移动
- 纯右值(prvalue):不具名且可被移动
- 泛左值(glvalue):具名,lvalue和xvalue都属于glvalue
- 右值(rvalue):可被移动的表达式,prvalue和xvalue都属于rvalue
左值 (lvalue,left value)
顾名思义就是赋值符号左边的值。准确来说,左值是表达式结束(不一定是赋值表达式)后依然存在的对象。
可以将左值看作是一个关联了名称的内存位置,允许程序的其他部分来访问它。在这里,我们将 “名称” 解释为任何可用于访问内存位置的表达式。所以,如果arr
是一个数组,那么arr[1]
和*(arr+1)
都将被视为相同内存位置的“名称”。
特征
- 可通过取地址运算符获取其地址
- 可修改的左值可用作内建赋值和内建符合赋值运算符的左操作数
- 可以用来初始化左值引用
举例
- 变量名、函数名以及数据成员名
int a = 5
- 返回左值引用的函数调用
T& f(); f();
- 由赋值运算符或复合赋值运算符连接的表达式,
a=b;
- 解引用表达式
int *ptr = &a;
- 前置自增和自减表达式
++a;
- 成员访问
.
运算符的结果obj.m
- 由指针访问成员
->
运算符的结果p->m
- 下标运算符的结果
[]
arr[0] = 10
- 字符串字面值
"abc"
对于一个表达式,凡是对其取地址(&)操作可以成功的都是左值
纯右值 (pvalue, pure ravlue)
自C++11开始,纯右值(pvalue, pure ravlue)相当于之前的右值,字面值或者函数返回的非引用都是纯右值。
特征
- 等同于C++11之前的右值
- 不会是多态
- 不会是抽象类型或数组
- 不会是不完全类型
举例
- 字面值(字符串字面值除外)
1; 'a'; true;
- 返回值为非引用的函数调用或操作符重载
int f(); f(); str1 + str2; it++;
- 后置自增和自减表达式
a++;
- 算术表达式
a+b;
- 逻辑表达式
a && b;
- 比较表达式
a > b;
- 取地址表达式
&a;
lambda
表达式[]{};
将亡值(xvalue, expiring value)
顾名思义即将消亡的值,是C++11
新增的跟右值引用相关的表达式,通常是将要被移动的对象(移为他用),比如返回右值引用T&&
的函数返回值、std::move
的返回值,或者转换为T&&
的类型转换函数的返回值。
将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。(通过右值引用来续命)。
xvalue 只能通过两种方式来获得,这两种方式都涉及到将一个左值赋给(转化为)一个右值引用
- 返回右值引用的函数的调用表达式,如
T&& f(); f();
该表达式得到一个xvalue
- 转换为右值引用的转换函数的调用表达式,如:
std::move(t)、satic_cast<T&&>(t)
1 | std::string fun() { |
在函数fun()
中,str
是一个局部变量,并在函数结束时候被返回。
在C++11之前,s = fun();
会调用拷贝构造函数,会将整个str
复制一份,然后把str
销毁。如果str
特别大的话,会造成大量额外开销。在这一行中,s
是左值,fun()
是右值(纯右值),fun()
产生的那个返回值作为一个临时值,一旦str
被s
复制后,将被销毁,无法获取、也不能修改。
自C++11开始,引入了move
语义,编译器会将这部分优化成move
操作,即不再是之前的复制操作,而是move
。此时,str
会被进行隐式右值转换,等价于static_cast<std::string&&>(str)
,进而此处的s
会将fun
局部返回的值进行移动。
无论是C++11之前的拷贝,还是C++11的move
,str
在填充(拷贝或者move
)给s
之后,将被销毁,而被销毁的这个值,就成为将亡值。
将亡值就定义了这样一种行为:具名的临时值、同时又能够被move
。
泛左值(glvalue, generalized lvalue)
又称为广义左值,是具名表达式,对应了一块内存。glvalue
有lvalue
和xvalue
两种形式。
特征
- 可以自动转换成
prvalue
- 可以是多态的
- 可以是不完整类型,如前置声明但未定义的类类型
右值(rvalue, right value)
是指可以移动的表达式,prvalue
和xvalue
都是rvalue
。
特征
- 无法对
rvalue
进行取地址操作。例如:&1; &(a + b);
,这些表达式没有意义,也编译不过 rvalue
不能放在赋值或者组合赋值符号的左边。例如:3 = 5; 3 += 5;
这些表达式没有意义,也编译不过rvalue
可以用来初始化const
左值引用。例如:const int& a = 1;
rvalue
可以用来初始化右值引用rvalue
可以影响函数重载:当被用作函数实参且该函数有两种重载可用,其中之一接受右值引用的形参而另一个接受const
的左值引用的形参时,右值将被绑定到右值引用的重载之上
例
前置自增(减)是左值,后置自增(减)是纯右值
1 | int i = 0; |
在上面代码中,我们定义了一个int类型的变量i,并初始化为0。
- ++i的操作是对i加1后再赋值给i,所以++i的结果是具名的,名称就是i,所以++i是左值
- 对于i++而言,先将i的值进行拷贝(此处假设拷贝到临时变量ii),然后再对i加1,最后返回ii(其实不存在的,为了在此表述方便)。所以i++是不具名的,因此不是glvaue,所以i++是右值,又因为不具名,且是右值,所以i++是纯右值
- 同理,–i是左值,i–是纯右值
算术表达式是纯右值
1 | int x = 0; |
在上述代码中,x + y
得到的是一个不具名的临时对象,所以x+y
是纯右值;而x && y
和x == y
得到的是一个bool
常量值,要么是true
要么是false
,所以是纯右值。
解引用是左值,取地址是纯右值
1 | int x = 0; |
*y
得到的是y
指向地址的实际值,所以&(*y)
是合法的,因此*y
是左值;对&y
操作得到的是一个地址,即一个long
值,所以是一个字面值,因此&y
是纯右值。
字符串字面值是左值
1 | std::cout << &"abc" << std::endl; |
字符串字面值为左值,这个比较特殊。在前面提到过字面值都是纯右值(字符串字面值除外),一个很重要的原因,就是可以字符串字面值可以获取地址,
这是因为C++将字符串字面值实现为char
型数组,实实在在地为每个字符都分配了空间并且允许程序员对其进行操作。如果从存储区的概念来理解,那就是字符串字面值存储在常量区。
左值引用
1 | int a = 5; |
右值引用
右值引用的引入,使得可以延长右值的生命周期。在C++
中规定,右值引用是&&
即由2个&
表示,而左值引用是一个&
表示。右值引用的作用是为了绑定右值
右值引用虽然是引用右值,但是其本身是左值
1 | int &&a = 1; |
在上述代码中,a
是一个右值引用,但是其本身是左值
a
出现在等号=
的左边- 可以对
a
取地址
判断是否是左值引用和右值引用
1 | int a = 5; |