左值引用和右值引用的区别是什么?如何声明一个右值引用?
这个问题,几乎是每个学移动语义的人都会碰到的第一道坎。我记得自己刚接触C++11那会儿,看到这两个概念也是一头雾水。说白了,左值引用和右值引用的核心区别就一句话:左值引用绑定到持久对象,右值引用绑定到临时对象。
但光知道这句话还不够。咱们得深入看看,它们到底怎么用,又有什么坑。
先说说左值引用
左值引用,就是咱们平时最常用的那种引用。用&来声明。它绑定到一个有名字、有地址的对象上。
int a = 42;
int& ref = a; // 左值引用,绑定到变量a
ref = 100; // 修改a的值
这里ref就是a的别名。你操作ref,就是在操作a。这个大家都很熟悉了。
左值引用有个特点:不能绑定到临时对象。比如下面这样写,编译器会报错:
int& ref = 42; // 错误!42是右值,不能绑定到左值引用
嗯,这里有个例外——const左值引用可以绑定到右值。这个咱们后面再说。
再来看右值引用
右值引用是C++11引入的新特性。用&&来声明。它专门用来绑定到临时对象、即将销毁的对象。
int&& rref = 42; // 正确!42是右值,可以绑定到右值引用
你想想看,42这个字面量,没有名字,没有地址,用完就没了。右值引用就是为这类对象准备的。
声明右值引用的语法很简单:
类型&& 变量名 = 右值表达式;
比如:
double&& d = 3.14;
std::string&& s = std::string("hello");
std::vector<int>&& v = std::vector<int>{1, 2, 3};
核心区别在哪里?
我整理了一个表格,方便你对比:
| 对比项 | 左值引用 (T&) | 右值引用 (T&&) |
|---|---|---|
| 声明符号 | & | && |
| 绑定对象 | 左值(有名字、有地址) | 右值(临时对象、字面量) |
| 能否修改 | 可以(非const时) | 可以 |
| 生命周期 | 与原对象绑定 | 可延长临时对象的生命周期 |
| 主要用途 | 避免拷贝、修改外部变量 | 移动语义、完美转发 |
这里有个容易混淆的点:右值引用本身是左值。什么意思呢?
int&& rref = 42;
int& lref = rref; // 正确!rref本身是左值,可以绑定到左值引用
为什么会这样?因为rref有名字,有地址,它就是一个变量。虽然它绑定到右值,但它自己是个左值。这个特性在完美转发中特别重要,后面我们会详细讲。
const左值引用:一个特殊的角色
我刚才提到,const T&可以绑定到右值。这是C++为了兼容旧代码保留的特性。
const int& ref = 42; // 正确!const左值引用可以绑定到右值
但注意,你不能通过const引用修改值。所以它只能用于读取,不能用于移动语义。
我在项目中遇到过这样的场景:一个函数参数写成const std::string&,既能接收左值,也能接收右值。但如果你想要移动语义,就必须用std::string&&。
避坑指南
我曾经踩过一个坑,分享给你:
std::vector<int> getVector() {
return std::vector<int>(1000000);
}
void process(std::vector<int>&& v) {
// 处理v...
}
int main() {
std::vector<int> data = getVector();
process(data); // 错误!data是左值,不能绑定到右值引用
}
这里data是左值,不能直接传给右值引用参数。你需要用std::move把它转成右值:
process(std::move(data)); // 正确!std::move将左值转为右值
嗯,这里要注意:std::move并不移动任何东西,它只是做了一个类型转换——把左值转成右值引用。真正的移动发生在移动构造函数或移动赋值运算符里。
知识结构图
下面这张图帮你理清左值引用和右值引用的关系:
什么时候用右值引用?
我个人习惯在以下几种场景使用右值引用:
- 移动构造函数和移动赋值运算符:这是最典型的用法,实现资源的高效转移。
- 完美转发:结合模板和
std::forward,保持参数的值类别。 - 函数重载:提供左值和右值两个版本,优化性能。
举个例子,一个简单的移动构造函数:
class Buffer {
char* data;
size_t size;
public:
// 移动构造函数
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size) {
other.data = nullptr; // 置空源对象
other.size = 0;
}
};
这里的关键是:我们从other偷走了资源,然后把other置空。这样other析构时就不会释放我们已经拿走的资源了。
总结一下
左值引用和右值引用,说白了就是两种不同的绑定规则。左值引用绑定到持久对象,右值引用绑定到临时对象。声明右值引用用&&,就这么简单。
但真正用好它们,需要理解背后的语义——什么时候该用左值引用,什么时候该用右值引用,什么时候该用std::move。这些我们会在后面的章节逐一展开。
&,右值用&&,不确定的时候用const T&。等用熟了再深入理解背后的原理。
std::move!因为右值引用本身已经是右值引用了,再move一次反而可能出问题。记住:std::move只应该用于左值。
公众号:蓝海资料掘金营,微信deep3321