C++98/03 只有左值引用 &,拷贝对象时会产生大量不必要深拷贝,性能损耗严重。C++11 引入右值引用 &&,诞生移动语义、移动构造、移动赋值,通过转移资源所有权替代拷贝,大幅减少内存拷贝开销。
1. 左值与右值
1.1 左值(lvalue,locatevalue)
- 可以取地址,能够修改的持久存在对象,生命周期长
int a = 10;
a = 20; // a 左值,可赋值、可取 &a
int arr[5];
arr[0] = 1; // arr[0] 左值
string s = "abc";
s[0] = 'x'; // s[0] 左值
1.2 右值(rvalue,readvalue)
- 纯右值(prvalue):字面量、运算表达式、函数返回临时对象
100; 3.14; "hello"; a + b; string("temp");
- 将亡值 xvalue:通过 std::move 转换后的对象,即将销毁、可移动资源
2. 左值引用 & 与右值引用 && 核心特性
2.1 左值引用 T&
- 只能绑定左值,不能绑定临时右值:
int a = 10;
int& r1 = a; // 绑定左值
int& r2 = 20; // 报错,不能绑定右值字面量
// const 左值引用特殊兼容:可绑定左值/右值
const int& r3 = 20; // 允许,临时对象生命周期延长
2.2 右值引用 T&&(C++11 新增)
- 只能绑定右值(纯右值 / 将亡值),专门捕获临时对象:
int&& rr1 = 100; // 绑定纯右值
int a = 10;
int&& rr2 = a; // 报错,a是左值,不能绑定普通左值
int&& rr3 = std::move(a);// move将左值转为将亡值,可绑定
2.3 && 特性:模板折叠引用
- 函数模板中 T&& 不是单纯右值引用,会根据实参类型自动折叠:
- 传入左值 T& → 折叠为 T& && = T&(左值引用)
- 传入右值 T → 折叠为 T&& && = T&&(右值引用)
template<typename T>
void func(T&& val) // 万能引用,既能接收左值也能接收右值
{
}
int main()
{
int x = 10;
func(x); // x左值 → T=int&,参数类型 int&
func(20); // 20右值 → T=int,参数类型 int&&
}
- 只有模板参数 T&& 才是万能引用;固定类型 string&& 只是单纯右值引用,无折叠特性。
2.4 右值引用延长临时对象生命周期
- 普通临时对象语句结束立刻销毁;绑定给 T&& 后,临时生命周期和引用变量同步:
string&& s = string("临时字符串");
// 临时对象不会立刻销毁,直到s离开作用域
3. 为什么需要右值引用 &&?
3.1 传统拷贝构造产生昂贵深拷贝,性能差
- 自定义类持有堆内存、容器资源时,拷贝构造会完整复制一份堆数据,临时对象拷贝后立刻销毁,拷贝完全多余。
示例:无移动语义的低效拷贝
class Str
{
public:
char* buf;
Str(const char* str)
{
buf = new char[strlen(str)+1];
strcpy(buf, str);
cout << "构造:开辟堆内存" << endl;
}
// 拷贝构造:深拷贝,重新分配内存
Str(const Str& other)
{
buf = new char[strlen(other.buf)+1];
strcpy(buf, other.buf);
cout << "拷贝构造:重新分配堆内存(开销大)" << endl;
}
~Str() { delete[] buf; cout << "析构:释放堆内存" << endl; }
};
Str createTemp()
{
return Str("测试临时对象"); // 返回临时右值
}
int main()
{
Str s = createTemp();
// C++98流程:1.创建临时Str 2.拷贝构造s 3.销毁临时对象
// 两次内存分配、两次释放,拷贝完全多余!
return 0;
}
3.2 无法复用即将销毁的临时对象资源
- 临时对象马上要析构释放堆内存,但拷贝构造依然重复开辟内存,硬件资源白白浪费。
- 右值引用的解决方案
捕获临时右值,新增移动构造函数:不拷贝资源,直接转移堆内存指针所有权,零拷贝开销。
4. 移动构造函数 & 移动赋值函数
4.1 移动构造函数语法
- 参数为本类右值引用 类名&&,作用:窃取传入临时对象的资源,置空原对象避免析构重复释放。
类名(类名&& 源对象) noexcept //noexcept :告诉编译器该函数无异常,标准库容器优化内存分配。
{
// 1. 直接转移堆资源指针
// 2. 将源对象指针置空,防止源对象析构释放内存
}
#include <iostream>
#include <cstring>
using namespace std;
class Str
{
public:
char* buf;
// 普通构造
Str(const char* str)
{
buf = new char[strlen(str)+1];
strcpy(buf, str);
cout << "构造:开辟堆内存\n";
}
// 拷贝构造:深拷贝
Str(const Str& other)
{
buf = new char[strlen(other.buf)+1];
strcpy(buf, other.buf);
cout << "拷贝构造:深拷贝\n";
}
// 移动构造:右值引用参数,转移资源
Str(Str&& other) noexcept
{
// 直接抢夺堆内存指针,不重新分配
buf = other.buf;
// 源对象置空,析构时不会释放内存
other.buf = nullptr;
cout << "移动构造:转移资源,无内存拷贝\n";
}
// 移动赋值运算符
Str& operator=(Str&& other) noexcept
{
if(this == &other) return *this; // 防止自赋值
// 释放当前对象旧资源
delete[] buf;
// 转移资源
buf = other.buf;
other.buf = nullptr;
cout << "移动赋值:转移资源\n";
return *this;
}
~Str()
{
if(buf != nullptr)
{
delete[] buf;
cout << "析构:释放堆内存\n";
}
else
{
cout << "析构:空对象,无需释放\n";
}
}
};
Str createTemp()
{
return Str("临时字符串");
}
int main()
{
Str s = createTemp();
// 触发移动构造,仅一次内存开辟,无多余拷贝
Str s2("第二个字符串");
s2 = createTemp(); // 触发移动赋值
return 0;
}
4.2 移动语义触发规则
- 传入纯右值(临时对象):优先调用移动构造 / 移动赋值
- 传入普通左值:只能调用拷贝构造,不会触发移动
- 手动使用 std::move(左值):将左值强制转为将亡值,主动触发移动
Str a("原始对象");
Str b = a; // 左值,调用拷贝构造
Str c = move(a); // move转换为将亡值,调用移动构造,a资源被掏空
4.3 移动构造 vs 拷贝构造对比表
| 特性 | 拷贝构造 const T& | 移动构造 T&& noexcept |
|---|---|---|
| 参数绑定 | 左值、const 右值 | 仅右值 /std::move转换的将亡值 |
| 资源处理 | 深拷贝,重新分配堆内存 | 转移指针所有权,无内存分配 |
| 性能开销 | 高,大量内存拷贝 | 极低,仅拷贝指针 |
| 原对象状态 | 资源完整保留 | 资源被掏空,变为空有效对象 |
| 适用场景 | 需要保留原对象数据 | 临时对象、不再使用的左值 |
5. std::move 原理
- std::move 本质只是强制类型转换:把任意对象转为对应类型的右值引用,不移动任何资源,只是给编译器信号 “这个对象可以被移动”。
template<typename T>
decltype(auto) move(T&& val)
{
return static_cast<remove_reference_t<T>&&>(val);
}
6. std::forward
- 万能引用参数如果直接传递会变成左值,std::forward
保留实参左右值属性:
template<typename T>
void wrapper(T&& arg)
{
target(std::forward<T>(arg)); // 完美转发,右值仍为右值,触发移动
}
浙公网安备 33010602011771号