核心要点速览
- 左值 vs 右值:左值有持久地址(可 & 取址),右值是临时对象 / 字面量(不可 & 取址)
- 右值引用(T&&):绑定右值,延长其生命周期,支持修改绑定对象
- 移动语义:通过移动构造 / 赋值转移资源(而非拷贝),提升性能(避免深拷贝)
- std::move:将左值转为右值引用(仅转换,不移动资源),原对象不应再使用
- 完美转发:std::forward 保持参数左值 / 右值属性,用于模板传递参数
一、左值与右值:值的分类
1. 左值
- 定义:可放在赋值左侧,有持久内存地址,生命周期较长(如变量、函数返回的左值引用)。
- 特征:可被
&取址(&a合法),可被赋值(a = 5合法)。 - 示例:
int x = 10;(x 是左值)、int& func()(返回左值引用,是左值)。
2. 右值
- 定义:只能放在赋值右侧,无持久内存地址(或地址无意义),生命周期短暂。
- 分类:
- 纯右值:字面量(
5、"hello")、表达式结果(x + y)、临时对象(func()返回非引用时)。 - 将亡值:即将被销毁的对象(如返回局部对象的函数返回值)。
- 纯右值:字面量(
- 特征:不可被
&取址(&5编译报错),通常是临时结果。
二、右值引用(T&&):绑定右值的引用类型
特性
- 绑定对象 :仅能绑定右值(纯右值或将亡值),不能直接绑定左值(需通过
std::move转换)。 - 生命周期延长:绑定临时对象后,临时对象生命周期延长至与右值引用相同(避免被立即销毁)。
- 可修改性 :与
const T&(常量左值引用,只读)不同,T&&可修改绑定的右值(因右值即将销毁,修改无副作用)。
三种引用对比
| 引用类型 | 语法 | 可绑定对象 | 能否修改绑定值 | 典型用途 |
|---|---|---|---|---|
| 左值引用 | T& |
左值 | 能(非 const) | 传递参数、返回引用 |
| 常量左值引用 | const T& |
左值、右值 | 不能 | 接收任意值(避免拷贝) |
| 右值引用 | T&& |
右值(纯右值、将亡值) | 能 | 实现移动语义、完美转发 |
| 常量右值引用 | const T&& |
右值 | 不能 | 禁止移动操作(极少使用) |
万能引用与引用折叠
- 万能引用 :仅当
T&&出现在模板参数推导场景(如template<typename T> void func(T&& param))时,才是万能引用,可绑定左值和右值;非模板场景下T&&就是普通右值引用(如void func(int&& param))。 - 引用折叠规则 :C++ 禁止直接声明引用的引用,编译器会自动折叠,原则为左值引用优先
- 若任一引用为左值引用(&),最终结果为左值引用(如
int& && → int&) - 仅当两个都是右值引用(&&),结果才为右值引用(如
int&& && → int&&)
- 若任一引用为左值引用(&),最终结果为左值引用(如
- 作用:是完美转发的底层实现原理,决定了模板参数的最终引用类型。
三、移动语义:避免冗余拷贝
1. 移动构造函数与移动赋值运算符
移动构造函数
- 语法:
T(T&& other) noexcept; - 作用:接管
other的动态资源(如指针指向的内存),将other置为 "可安全销毁" 状态(如指针置空)。 - 示例逻辑:
cpp
String(String&& other) noexcept : str(other.str) {
other.str = nullptr; // 掏空原对象,避免析构时重复释放
}
- 默认移动构造 / 赋值的生成条件:
- 类未自定义拷贝构造、拷贝赋值、析构、移动构造、移动赋值中的任意一个;
- 所有非静态数据成员和基类可被移动语义处理(允许部分成员拷贝,不影响生成)。
移动赋值运算符
- 语法:
T& operator=(T&& other) noexcept; - 作用:先释放当前对象资源,再接管
other的资源,最后将other置空。 - 注意:需处理自赋值场景,避免资源提前释放。
2. 优势
- 性能优化:将深拷贝(内存分配 + 数据复制)简化为指针赋值,大幅提升效率。
- 资源安全:针对将亡值(如临时对象),转移资源不影响其他对象。
- 支持不可拷贝对象的转移:某些资源(如文件句柄)不可拷贝,但可通过移动转移所有权。
四、std::move:左值转右值引用(非实际移动)
- 作用 :强制将左值转换为右值引用(仅修改值的属性,不实际移动资源),底层本质是
static_cast<T&&>。 - 特性 :
- 转换后原对象仍 "有效但不应再使用"(资源可能已被转移)。
- 可用于任何对象(内置类型、自定义类型),无性能开销。
- 对 const 对象无效:
const T调用std::move后仍是const T&&,无法触发移动构造,会调用拷贝构造。
- 示例:
String s1; String s2 = std::move(s1);(s1 转为右值引用,触发 s2 的移动构造)。
五、完美转发:保持参数值类别
- 问题:模板中传递参数时,左值 / 右值属性可能丢失(如右值被转为左值引用)。
- 解决方案 :
std::forward<T>(t),根据T的类型保持参数的左值 / 右值属性,仅在模板万能引用场景下有效。 - 与 std::move 的区别 :
std::move:无条件将左值转右值,仅用于触发移动语义std::forward:有条件转发,仅在模板中根据参数原始类型保持属性
- 典型场景:模板转发参数至内部函数,确保参数类型正确传递:
cpp
template<typename T>
void wrapper(T&& t) {
func(std::forward<T>(t)); // 保持t的左值/右值属性
}
六、补充
- noexcept 的作用 :移动构造 / 赋值若加
noexcept,标准容器(如vector)扩容时会优先选择移动而非拷贝(避免异常导致数据丢失),否则可能 fallback 到拷贝,失去优化意义。 - 移动语义 vs 拷贝语义:拷贝是 "复制资源,原对象不变";移动是 "转移资源,原对象失效"。
- 右值引用为何能提升性能:针对临时对象(右值),无需拷贝其资源,直接转移所有权,消除冗余的内存分配和复制。
- RVO/NRVO 与移动语义的关系 :
- 返回值优化(RVO):C++17 后强制,编译器直接在调用者内存构造返回对象,跳过拷贝 / 移动构造
- 命名返回值优化(NRVO):优化命名局部变量返回,非标准强制,编译器通常支持
- 注意:RVO/NRVO 优先级高于移动语义,若触发优化,不会调用移动构造函数
- 移动语义的常见陷阱 :
- 移动后原对象仅保证可析构,不可再访问其资源(如空指针解引用)
- 浅拷贝对象(无动态资源)使用移动语义无性能提升,反而增加代码复杂度
- 类中自定义析构函数会导致默认移动函数失效,需手动实现