引言
左值和右值的概念一直令C++新手摸不着头脑。今天这个视频我要给大家讲讲C++中的左值和右值以及左值引用和右值引用。

什么是左值?
左值是可以出现在赋值操作符左侧的对象,哈哈,是不是说了句费话。这个定义人人都知道。
其实我们可以从C++编译器的角度来理解,那C++的编译器是如何判断的呢?编译器通过表达式是否能取地址来区分左值和右值。
左值就是可以取地址的表达式,它通常是有名字的、持久的对象。例如:
int a = 10;
编译器为变量a分配了一块内存空间。并且你可以用 &a 获取它的内存地址。它有明确的身份,你可以使用a来读写值,因此A就是一个左值。
什么是右值?
而这个表达式中的10它没有变量名,它只是一个值,你不能在代码中直接通过名字引用它。在这个语句中它只是一个立即数,用完就没了。它的“存在”仅限于初始化 a 的那一瞬间。你更不能这样 &10 来获取它的地址。因此它就是一个右值。
右值通常来自例如字面量,像10,3.14, true,或者表达式计算的结果例如5+3,或者使用std::move(a)将左值转换为右值。
左值引用
对应左值和右值,还有2个让新手不好理解的概念,左值引用和右值引用。
左值引用是对左值的别名,用 & 表示。它只能绑定到左值。例如:
int a = 10;
int& ref = a;
这个表达式中,ref 是 a 的别名。左值引用可以避免拷贝,左值引用本质上是指针的语法糖。这很好理解是吧。
右值引用
而右值引用就没那么好理解了。右值引用,用双 && 表示,例如 int&& =5;。
右值引用是对右值的引用,主要绑定到临时对象或即将销毁的对象。右值引用的引入主要为了支持移动语义,它允许你“窃取”右值内部资源,使它留下空壳,从而避免深拷贝。
移动语义示例
我们来看一个例子:
#include <iostream>
#include <utility>
class MyString { char* data; public: MyString(const char* s) { data = new char[std::strlen(s) + 1]; std::strcpy(data, s); } // 移动构造函数 MyString(MyString&& other) noexcept : data(other.data) { other.data = nullptr; // 资源转移 } ~MyString() { delete[] data; } void print() const { std::cout << (data ? data : “null”) << std::endl; } };
int main() { MyString s1(“Hello”); MyString s2 = std::move(s1); // 移动构造 s1.print(); // 输出 “null” s2.print(); // 输出 “Hello” }
我们有一个MyString的类,它有一个data成员变量。在构造 函数中我们为data申请一块内存。然后我们在移动构造函数中会将另一个对象的data的内存转移给自己,并使另一个类对象的data变成空。从而使另一个类对象只留 下一个空壳。
在main函数中我们定义一个MyString的类对象s1。然后在这里,我们使用move将s1从一个左值变成右值,并赋值给s2。此时会触发移动构造函数的调用,这会使得s1变成一个空壳,而s2则获得原来s1所拥有的内存。
右值引用与移动语义的优势
那这样做有什么好处呢?其实一切都是为了性能,反复的内存申请和释放会使得代码性能下降,而右值引用和移动语义的引入大大提升了C++代码的性能。
总结
关于左值和右值以及左值引用,右值引用你现在明白了没?


