什么是左值?什么是右值?如何区分它们?
面试官: 在 C++ 中,我们经常听到“左值”和“右值”这两个术语。你能用自己的话,简单地解释一下,什么是左值,什么是右值吗?最简单的区分它们的方法是什么?
回答:我在一年多前的程序设计课上学过这两个概念,但现在我的记忆已经模糊,凭感觉回答一下。
- 左值是被持久保存而不是临时保存的值,它通常就是我们定义的变量的值。因为出现在赋值式子的左边,所以叫做“左值”。
- 右值是通过某些代数计算式得到的临时值,它通常出现在赋值式子的右边,因此被称为“右值”。
面试官 (补充与深化): 你说的“能否放在赋值运算符左边”是一个非常好的启发式判断方法。一个更根本的、适用于现代 C++ 的区分标准是:
我们能否对这个表达式“取地址”?
- 左值 (lvalue):是一个表达式,它指向了一个具名的、有持久状态的对象或函数。我们可以对它取地址 (
&)。它在表达式结束后,依然存在。- 例子:变量名
int x;x是一个左值,我们可以&x。std::string s;s是一个左值。
- 例子:变量名
- 右值 (rvalue):是一个表达式,它通常代表一个匿名的、即将被销毁的临时值。我们无法对它取地址。它在表达式结束后,就不复存在了。
- 例子:字面量
10是一个右值,你不能&10。函数返回值get_string()(如果返回的是一个值而非引用)是一个右值。x + y这个表达式的结果也是一个右值。
- 例子:字面量
总结一下:
- 左值:有家(内存地址),能活得久。
- 右值:没家(没法取地址),是“过客”(马上就消失)。
右值引用
面试官 (引入新概念): 好的,既然我们知道了左值和右值的区别。C++11 为了配合移动语义,引入了一种新的引用类型,叫做右值引用 (rvalue reference),用 && 表示。
| |
请问,&& 这个右值引用的主要设计目的是什么?它和我们之前讨论的移动语义 (Move Semantics) 是如何关联起来的?
回答:
- “移动语义”就是我们之前提过的,“移动资源,而不是重新申请资源,以降低开销的方法”。使用移动语义的前提是这份资源后续不再被人占用。
- 右值引用能让编译器识别到该引用的对象为右值,它占用的资源即将被销毁,之后没人再用了。
- 这就是在告诉编译器,如果需要针对这个对象进行赋值操作,就可以放心大胆地用“移动语义”来直接转移资源,降低开销。
面试官 (总结与深化): 你说得完全正确。我来帮你把这个逻辑和具体的函数联系起来。
右值引用的出现,使得我们能够重载 (overload) 函数,为左值和右值参数提供不同的实现版本:
| |
- 当编译器看到
s1 = s2;时,它发现s2是一个具名的变量,是一个左值。于是,它在MyString的成员函数里查找,找到了最匹配的版本operator=(const MyString& other)。 - 当编译器看到
s1 = MyString("temporary");时,它发现MyString("temporary")是一个没有名字的、表达式结束就会被销毁的右值。于是,它找到了一个更匹配的版本operator=(MyString&& other),并调用它。
结论:
右值引用 (&&) 是一种新的函数参数类型,它使得函数重载机制能够区分开左值和右值。这为移动语义的实现提供了语法基础,允许我们为短暂的右值对象编写专门的、高效的“资源窃取”版本,从而极大地提升了 C++ 代码的性能。
手动触发移动
面试官 (最终问题): 好的,你对右值引用和移动语义的理解已经非常深入了。最后一个小问题。
我们知道可以用 std::move 来将一个左值强制转换为一个右值引用,从而“手动”触发移动操作。
| |
请问,std::move(s1) 这个函数调用,它本身做了什么?它真的“移动”了 s1 吗?调用之后,s1 的状态会立刻改变吗?
回答:我不太清楚这个问题的答案,凭直觉简单回答,请你补充。
std::move(s1)是强制类型转换,给了s1一个右值身份,让编译器将s1视为右值处理。s1并没有被移动。s1的状态不会立刻改变。- 真正的移动操作,是在移动赋值运算符的内部(也就是等号所表示的函数的内部)那里发生的。
面试官 (解释与澄清): 你说得对,std::move 本身什么也没移动。
std::move 的本质,仅仅是一个类型转换 (cast)。
它唯一的作用,就是无条件地将一个输入(无论左值还是右值)转换成一个右值引用 (&&)。
它就像是在对编译器说:
“嘿,编译器,我知道
s1是一个左值,但在这里,请你假装它是一个右值。我向你保证,我接下来不会再以有意义的方式使用s1了,你可以放心地对它进行移动操作(资源窃取)。”
我们来分解 s2 = std::move(s1); 这一行的完整过程:
std::move(s1)执行:s1(一个MyString类型的左值)被传入std::move。std::move返回一个MyString&&类型的右值引用,这个引用依然指向s1。- 在这一刻,
s1内部的数据没有任何改变。std::move只是做了一个“演员认证”,给了s1一个“右值”的临时身份。
operator=执行:- 现在,编译器需要为
s2 = ...选择一个赋值运算符的重载版本。 - 等号右边的表达式
std::move(s1),它的类型是MyString&&(右值引用)。 - 因此,编译器选择了移动赋值运算符
operator=(MyString&& other)。 s2的移动赋值运算符被调用,参数other绑定到了s1上。
- 现在,编译器需要为
移动赋值运算符内部:
- 在这个函数内部,真正的“移动”操作才发生。
- 它会“窃取”
s1的内部资源(比如指针),然后把s1的内部状态设置为“空”。 - 函数执行完毕后,
s2拥有了原来的资源,而s1变成了一个空的字符串。
总结:
std::move不移动,只进行类型转换,把左值伪装成右值。- 真正的移动操作,是由对象的移动构造函数或移动赋值运算符完成的。
std::move只是为后续调用这些移动成员函数创造了条件。
总结
面试结束
今天我们深入探讨了 C++ 中一个既基础又深刻的话题:左值与右值。
- 我们理解了区分它们的现代方法(能否取地址)。
- 学习了右值引用的作用(为函数重载提供识别右值的类型)。
- 并最终澄清了
std::move的真正含义(仅仅是类型转换)。
这些知识是理解现代 C++ 高性能编程的关键。你今天的学习状态非常好,能通过直觉猜对很多复杂的概念,并且能快速吸收新的知识。继续保持!