C++ 左值引用与右值引用详解
在 C++ 的类型系统中,引用(reference) 是一种为已有对象起别名的机制。在早期(C++98/03)中,C++ 只有 左值引用(lvalue reference),主要用于函数参数传递、返回值以及避免拷贝。到了 C++11,引入了 右值引用(rvalue reference),为完美转发(perfect forwarding)和移动语义(move semantics)提供了语言支持,大大提升了性能和灵活性。
一、左值与右值的基本概念
在解释左值引用和右值引用之前,需要弄清楚 左值(lvalue) 和 右值(rvalue) 的含义。
1. 左值(lvalue)
- 左值表示在表达式结束后仍然存在的对象,可以取地址,有名字。
- 特点:可以出现在赋值号的左边(其实是因为可寻址,而不是单纯因为在左边)。
- 常见例子:
int x = 10; // x 是一个左值
x = 20; // 左值可以出现在赋值号的左边
&x; // 可以取地址
2. 右值(rvalue)
- 右值表示表达式结束后不再存在的临时对象,通常不能取地址,没有名字。
- 特点:只能出现在赋值号的右边(通常是字面量、表达式结果等)。
- 常见例子:
10; // 字面量是右值
x + 5; // 表达式结果是右值
std::string("Hello"); // 临时对象是右值
二、左值引用(lvalue reference)
1. 定义
左值引用的语法形式是在类型名后面加上 &
:
int a = 10;
int& ref = a; // ref 是 int 类型的左值引用,绑定到 a
此时 ref
就是 a
的别名,对 ref
的修改就是对 a
的修改。
2. 特点
- 只能绑定到左值。
- 一旦绑定,引用不可更换绑定对象。
- 常用于函数参数传递,避免拷贝,提高性能。
3. 示例
void increment(int& n) {n++;
}int main() {int x = 5;increment(x); // x 被修改为 6
}
三、右值引用(rvalue reference)
1. 定义
右值引用的语法形式是在类型名后面加上 &&
:
int&& rref = 10; // rref 绑定到右值 10
右值引用可以绑定到临时对象(右值),使得我们可以在它被销毁之前对其进行修改或“移动”。
2. 特点
- 只能绑定到右值(包括字面量、临时对象、表达式结果等)。
- 常用于移动语义和完美转发。
- 避免不必要的深拷贝,提高性能。
3. 示例
#include <iostream>
#include <vector>void printVector(std::vector<int>&& v) {std::cout << "Size: " << v.size() << '\n';
}int main() {std::vector<int> tmp = {1, 2, 3};printVector(std::move(tmp)); // 将 tmp 转换为右值引用
}
四、移动语义与右值引用
在 C++98/03 中,如果我们返回一个大对象,会产生不必要的深拷贝:
std::string getStr() {std::string s = "Hello";return s; // 旧标准中可能拷贝一次
}
C++11 引入了 移动构造函数 和 右值引用,允许“窃取”临时对象的资源,而不是复制。
移动构造函数示例
#include <iostream>
#include <string>class MyString {char* data;
public:MyString(const char* s) {data = new char[strlen(s)+1];strcpy(data, s);}// 移动构造函数MyString(MyString&& other) noexcept {data = other.data;other.data = nullptr;std::cout << "Moved!\n";}~MyString() { delete[] data; }
};int main() {MyString a("Hello");MyString b(std::move(a)); // 调用移动构造
}
五、完美转发与 std::forward
当我们写一个模板函数时,可能希望保留实参的值类别(左值或右值)。使用 右值引用 + 引用折叠规则 + std::forward
可以实现完美转发。
#include <utility>
#include <iostream>void process(int& x) { std::cout << "Lvalue\n"; }
void process(int&& x) { std::cout << "Rvalue\n"; }template<typename T>
void forwarder(T&& arg) {process(std::forward<T>(arg));
}int main() {int a = 5;forwarder(a); // 输出 Lvalueforwarder(10); // 输出 Rvalue
}
六、引用折叠规则
C++ 的引用折叠规则规定:
T& &
折叠为T&
T& &&
折叠为T&
T&& &
折叠为T&
T&& &&
折叠为T&&
这使得 万能引用(universal reference)(或者叫转发引用 forwarding reference)成为可能。
七、注意事项
- 右值引用不是万能的:绑定到右值意味着你可以修改它,但并不是强制要移动它。
- 使用
std::move
:std::move
并不移动对象,只是将左值强制转换为右值引用。 - 警惕悬挂引用:右值引用绑定到临时对象,如果临时对象销毁,引用就悬空。
- 移动后对象可用但状态不确定:移动语义通常会清空被移动对象的内部资源,但允许它被安全析构。
八、总结对比表
特性 | 左值引用 (T&) | 右值引用 (T&&) |
---|---|---|
可绑定对象类型 | 左值 | 右值 |
常见用途 | 参数传递,避免拷贝 | 移动语义,完美转发 |
C++ 引入版本 | C++98 | C++11 |
是否可更换绑定对象 | 否 | 否 |
是否支持取地址 | 是 | 是 |