C++11 系列(二):右值引用与移动语义完全指南
前言
C++11 引入了右值引用(T&&)和移动语义,这是 C++11 最重要的特性之一。它们解决了 C++98 中临时对象拷贝效率低下的问题,为现代 C++ 的高性能编程奠定了基础。
本文将从左值与右值的基本概念出发,系统讲解右值引用、移动构造/移动赋值、引用折叠、完美转发等核心知识。
一、左值与右值
1.1 基本概念
在 C++ 中,表达式可以分为左值和右值:
| 类型 | 含义 | 特点 | 示例 |
|---|---|---|---|
| 左值(lvalue) | 有持久状态、可取地址 | 可以出现在赋值左边 | 变量名、*p、s[0] |
| 右值(rvalue) | 临时值、不可取地址 | 只能出现在赋值右边 | 字面量、x+y、临时对象 |
现代 C++ 中,lvalue 被解释为 locator value (有地址的值),rvalue 被解释为 read value(仅可读的值)。
1.2 代码示例
cpp
int main() {
// 左值:可以取地址
int* p = new int(0);
int b = 1;
const int c = b;
string s("hello");
s[0] = 'x';
cout << &c << endl; // 可以取地址
cout << (void*)&s[0] << endl;
// 右值:不能取地址
double x = 1.1, y = 2.2;
10; // 字面量
x + y; // 表达式结果
string("temp"); // 临时对象
// cout << &10 << endl; // 错误:不能取地址
return 0;
}
二、左值引用与右值引用
2.1 基本语法
cpp
int a = 10;
int& lref = a; // 左值引用:绑定到左值
int&& rref = 20; // 右值引用:绑定到右值
2.2 引用绑定规则
| 引用类型 | 能否绑定左值 | 能否绑定右值 |
|---|---|---|
T&(左值引用) |
✅ | ❌ |
const T&(const 左值引用) |
✅ | ✅ |
T&&(右值引用) |
❌ | ✅ |
2.3 std::move:将左值转为右值
cpp
int a = 10;
int&& rref = std::move(a); // 将左值 a 转为右值引用
std::move 的本质是一个强制类型转换:
cpp
template<class T>
remove_reference_t<T>&& move(T&& arg) {
return static_cast<remove_reference_t<T>&&>(arg);
}
2.4 重要:右值引用变量本身是左值
cpp
int&& rref = 10; // rref 绑定到右值 10
int& lref = rref; // 正确:rref 本身是左值
// int&& rref2 = rref; // 错误:不能将右值引用绑定到左值
int&& rref2 = std::move(rref); // 正确:需要再次 move
⚠️ 关键理解:右值引用变量在表达式中被视为左值,因为它有名称、可取地址。
三、移动语义
3.1 为什么需要移动语义?
在 C++98 中,返回局部对象或插入临时对象时,会发生深拷贝,效率低下:
cpp
string addStrings(string num1, string num2) {
string str;
// ... 拼接逻辑
return str; // 需要拷贝,效率低
}
左值引用虽然能减少拷贝,但无法解决 返回局部对象 的拷贝问题。移动语义允许我们 窃取 临时对象的资源,而不是拷贝。
3.2 移动构造与移动赋值
cpp
class string {
public:
// 拷贝构造:深拷贝
string(const string& s) {
// 分配新内存,复制数据
}
// 移动构造:窃取资源
string(string&& s) noexcept {
_str = s._str; // 直接接管指针
s._str = nullptr; // 将源对象置空
}
// 移动赋值:窃取资源
string& operator=(string&& s) noexcept {
if (this != &s) {
delete[] _str;
_str = s._str;
s._str = nullptr;
}
return *this;
}
private:
char* _str;
};
3.3 使用场景
cpp
int main() {
bit::string s1("hello");
// 拷贝构造(s1 是左值)
bit::string s2 = s1;
// 移动构造(匿名对象是右值)
bit::string s3 = bit::string("world");
// 移动构造(move 将左值转为右值)
bit::string s4 = std::move(s1);
return 0;
}
3.4 移动语义在容器中的提效
C++11 后,STL 容器的 push_back、insert 等接口增加了右值引用版本:
cpp
list<string> lt;
string s1("hello");
lt.push_back(s1); // 拷贝构造
lt.push_back(string("world")); // 移动构造
lt.push_back("hello world"); // 移动构造
lt.push_back(std::move(s1)); // 移动构造
四、引用折叠
4.1 问题引入
C++ 不允许直接定义引用的引用,但在模板或类型别名中会出现:
cpp
typedef int& lref;
lref& r1 = n; // r1 的类型是什么?
4.2 折叠规则
C++11 定义了引用折叠规则:
| 原始类型 | 折叠后 |
|---|---|
T& & |
T& |
T& && |
T& |
T&& & |
T& |
T&& && |
T&& |
总结:只有右值引用的右值引用折叠成右值引用,其他全部折叠成左值引用。
4.3 万能引用(Universal Reference)
在模板中,T&& 可以根据实参推导为左值引用或右值引用:
cpp
template<class T>
void Function(T&& t) { // 万能引用
// t 的类型取决于实参
}
int main() {
int a = 10;
Function(10); // T = int,参数类型:int&&(右值引用)
Function(a); // T = int&,参数类型:int&(左值引用)
return 0;
}
五、完美转发
5.1 问题:参数类型丢失
cpp
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
template<class T>
void Function(T&& t) {
Fun(t); // t 是变量表达式,永远是左值!
}
Function(10); // 输出:左值引用(但预期应该是右值引用)
5.2 解决方案:std::forward
cpp
template<class T>
void Function(T&& t) {
Fun(std::forward<T>(t)); // 保持 t 的原始类型
}
5.3 forward 的实现原理
cpp
template<class T>
T&& forward(remove_reference_t<T>& arg) noexcept {
return static_cast<T&&>(arg);
}
- 如果
T是int&,则返回int& - 如果
T是int,则返回int&&
六、值类别(补充知识)
C++11 进一步细化了值类别:
expression
/ \
glvalue rvalue
/ \ / \
lvalue xvalue prvalue
| 类别 | 说明 | 示例 |
|---|---|---|
| lvalue | 左值 | 变量名、*p |
| xvalue(将亡值) | 即将被移动的对象 | std::move(x)、T&& 函数返回值 |
| prvalue(纯右值) | 字面量、临时对象 | 42、a+b、string("tmp") |
七、总结
| 特性 | 作用 |
|---|---|
左值引用 T& |
减少拷贝,可修改实参 |
右值引用 T&& |
绑定临时对象,实现移动语义 |
std::move |
将左值转换为右值引用 |
| 移动构造/赋值 | 窃取临时对象资源,避免深拷贝 |
| 引用折叠 | 模板中 T&& 的推导规则 |
完美转发 std::forward |
保持参数在传递过程中的原始类型 |
核心要点
- 右值引用变量本身是左值(有名称、可取地址)
- 移动语义只对深拷贝类型有意义 (如
string、vector) std::move只是类型转换,不移动任何东西std::forward用于模板中保持参数的原始类型- 移动构造/赋值通常应标记为
noexcept,以便 STL 容器优先使用