奶奶都能看懂的 C++ —— 左值和右值
如果你上网搜索过一些 C++ 教程,你总会遇见两个名词,左值和右值。这是什么意思呢?它们有什么区别呢?今天就来详细看看这两个概念。
顾名思义
我们先看看这个名字。左值,隐含的意思是“可以放在等号左边的变量”,而右值则不可以。实际上,在 C 语言中,确实是这样没错。但是在 C++ 中(由于一些例如常量的存在),问题就变得复杂了起来。
那到底怎么理解左值和右值呢?可以这么想:
左值表示的是一个可被取地址,识别、找到和编辑的对象(保存了内存位置,可以通过这个内存位置来找到这个对象,也允许编辑其值),而右值则表示一个对象的本身(也就是真正的值,但是位置不确定,通常是临时的,可能很快会被释放)。
左值和右值
这么说还是有点抽象,我们来看看一些例子:
1 | |
在上面的例子中,1 和 2 这两个数,就是我们所说的右值。它们并没有一个确定位置,它们的出生就是为了写入 i 这个变量。而 i 这个变量是我们用 int 声明并定义的,因此它在内存中有一个确切的位置,我们可以通过 i 来访问和修改。因此,它是一个左值。
我们来到第三行,左边的 b 很明显是个左值,但是右边的 i 是什么呢?我们知道它本身是个左值,但是它处于等号的右边,那么此时就是把左值当作右值来使用。
通常情况下,左值可以当作右值来使用(你可以提供一个有位置对应的对象,此时用的是对象本身)。但是,你不能把右值当作左值(明明需要一个有确定对应位置的对象,你却只给了一个值)。比如:
1 | |
很明显,赋值左侧需要左值,但却给定了一个右值。这显然是不符合逻辑的——你需要指定一个内存地址,才可以写入一个值,但你现在等号左侧的对象却是一个值,C++ 根本不知道你要写入哪个内存地址的对象。
引用
还记得我们之前提到的引用吗?
当你创建一个引用时,等号右侧必须是一个左值(根本不会出现左值到右值的转换)。
为什么?因为引用只是贴标签,不能给一个连地址都不确定的右值对象创建一个引用。
1 | |
常量
之前在 const 限定符一节,已经介绍了常量、常量指针和常量引用。那么它们是左值还是右值呢?
先说结论:除了字面量常量(例如 'c' 123 1.14),其余的 const 限定不会改变变量的左值本质。
1 | |
是的,上面的所有变量(常量)都是左值,而非右值。
当我们理解了左值和右值时,为什么(最后一行)字面量可以创建常量引用就显得清晰了——我们有一个右值,这个右值我们不知道内存地址因此无法更改,因此,我们把一个引用绑定到这个孤独的右值上(并且指出它不可修改的本质),我们就得到了一个左值引用。
你应该还记得引用本身无法修改,当它设定为 const 时,值也无法修改。我们创建这个左值常量引用,相当于延长了这个右值的寿命(但是并没有更改原本的那个临时右值的右值性质)。
地址和解引用
在上面的代码里,还出现了取地址符 &。这个符号就很有意思了,它传入一个左值,返回一个右值。相信你很快就理解了——通过一个对象找到对应对象的地址,不就是左值(传入一个有地址的对象)到右值(取得的对象地址是一个临时的值)的转变吗?
反过来,* 解引用符返回的则是一个左值。它代表通过地址查找对象,返回的是一个具有确定地址的对象,因此是一个左值。
此外,我们之前还提到过数组、vector 和其它有序序列,它们的下标运算,通常取得的也是左值。
从这个观点来看,除了我们之前纠结的是否为拷贝赋值,还要关注返回值是左值还是右值(通常来说,拷贝后产生的临时变量是右值,如果没有拷贝,产生的是左值)。
i++ 和 ++i
奶奶都知道,i++ 先返回 i,再自增;++i 先自增,再返回 i。
但当我们用左值、右值的观点来看时,它们的本质就出来了:
i++返回右值(返回原始 i 的一个临时副本),然后把 i 加一++i先把 i 加一,然后返回一个左值(也就是i)
也就是说,下面的代码是完全合法的:
1 | |
上面代码先定义一个 i,值为 1。然后把 i 加 1 变成 2,再把 3 赋值给 i 这个左值。
最终结果,i 变成了 3。
范围 for
我们之前提到过范围 for 的拷贝赋值问题,现在再用左值和右值的视角回顾一下这个问题(拿出一样的代码)。冒号前面的变量是否为引用,决定了是左值还是右值:
1 | |
第一种,默认行为,遍历时返回当前元素的临时右值副本。你可以这么理解,当成遍历时做了这种等效操作——
1 | |
vtmp 表示当前正在遍历的 v 中的一个元素,而当其处于等号右侧,是把左值当成右值(创建临时右值副本),然后赋值给 i 这个左值。
也就是说,修改 i 不改变原先 v 中的元素。
第二种,引用。
1 | |
这时,第一个范围 for 遍历时返回的,就是 v 中的一个元素本身(它是左值)。相当于我们之前提到的创建引用:
1 | |
第三种加上 const 就不必多说了,因为已经提到过,const 不会更改左值本质。
总结
通过探索左值和右值,你现在应该已经有了相当程度的认知。总结一下:
- 左值是一个有确定地址的对象,右值通常是临时的,不确定地址
- 左值通常可以当成右值(取值),但右值不能当成左值
- 引用初始化,两侧都是左值
- 取地址、解引用返回的分别是右值、左值
- 下标运算返回左值
- 前置 ++ 返回左值,后置返回右值
- 范围 for 相当于取出每一个元素然后赋值,左右值类型根据是否为引用决定







