一、什么是移动语义?
"C++ 的移动语义"对应的英文是Move Semantics。
移动语义是C++11引入的一项核心特性。它允许把资源(如堆内存、文件句柄、套接字等)**直接"转移"**给另一个对象,而不是"复制"一份。这样可以避免不必要的深拷贝,显著提升性能。
打个比方:搬家时,复制相当于把所有家具再买一套新的;移动则是直接把家具搬到新家,旧房子就空了。
二、为什么需要移动语义?
考虑下面的场景:函数返回一个很大的std::vector,或者把一个临时对象放进容器。在 C++11 之前,这些操作往往会触发完整的深拷贝:分配新内存、逐个元素复制、再释放旧内存。
但很多时候,源对象马上就要销毁(比如临时对象),复制它纯属浪费。移动语义的思路是:既然源对象不再需要了,直接"偷走"它的资源即可。
三、核心概念
1. 左值与右值
- 左值(lvalue):有名字、有持久地址的对象,可以取地址,如变量
a。 - 右值(rvalue):临时的、即将销毁的值,如字面量
42、函数返回的临时对象。
2. 右值引用(&&)
右值引用T&&专门用来绑定右值,从而让编译器能够区分"可以安全偷资源的对象"和"必须保留的对象"。这是实现移动语义的语法基础。
3. 移动构造函数(Move Constructor)
接受一个右值引用参数,把源对象的资源转移到新对象中。
4. 移动赋值运算符(Move Assignment Operator)
与移动构造函数类似,但用于对已存在的对象赋值。
5.std::move
std::move位于<utility>头文件中。它本身并不移动任何东西,只是把参数无条件地转换为右值引用,相当于告诉编译器:"这个对象可以被移走了。"真正的移动发生在随后调用的移动构造函数或移动赋值运算符中。
四、示例代码
原文代码中有若干被截断、损坏的地方(缺少普通构造函数的声明、main函数末尾的反引号错误等),下面是修正后的完整可运行版本:
#include<iostream>#include<vector>#include<utility>// std::moveclassMyClass{public:std::vector<int>data;// 普通构造函数explicitMyClass(std::size_t size):data(size){}// 移动构造函数MyClass(MyClass&&other)noexcept:data(std::move(other.data)){std::cout<<"Moved!\n";}// 移动赋值运算符MyClass&operator=(MyClass&&other)noexcept{if(this!=&other){data=std::move(other.data);}return*this;}};intmain(){MyClassobj1(1000);// 普通构造MyClass obj2=std::move(obj1);// 调用移动构造函数std::cout<<"obj1.data.size() = "<<obj1.data.size()<<"\n";std::cout<<"obj2.data.size() = "<<obj2.data.size()<<"\n";return0;}可能的输出:
Moved! obj1.data.size() = 0 obj2.data.size() = 1000注意:被移动后
obj1.data通常为空,但标准只保证它处于"有效但未指定"的状态,不要依赖具体的值。
代码解析
obj2通过移动构造函数创建,obj1内部vector的底层缓冲区指针被直接转移给obj2,没有逐元素复制 1000 个int。- 移动之后,
obj1仍然是一个有效但未指定状态的对象:可以安全销毁,也可以重新赋值,但不应再读取它原来的数据。 - 移动操作都标记了
noexcept,这很重要(见下文)。
五、错误修正记录
| 位置 | 原文问题 | 修正 |
|---|---|---|
| 右值引用说明 | 文字被截断:“lvalues … and r), enabling moves” | 补全为"区分左值与右值" |
| 示例代码 | 普通构造函数残缺(//_t size) : data(size) {}) | 补全为explicit MyClass(std::size_t size) : data(size) {} |
| 示例代码末尾 | main函数缺少闭合花括号,混入多余反引号 | 补全并规范代码块 |
| 结论句 | “leavingobj1in a valid” 句子不完整 | 补全为"有效但未指定的状态" |
六、扩展知识
1. 为什么要加noexcept?
标准库容器(如std::vector)在扩容时,只有当元素的移动构造函数声明为noexcept时,才会选择移动;否则为了保证强异常安全,它会退而求其次使用拷贝。忘记写noexcept会让你的移动优化悄悄失效。
2. 五法则(Rule of Five)
如果一个类需要自定义以下任意一个,通常应当考虑把五个都定义(或显式= default/= delete):
- 析构函数
- 拷贝构造函数
- 拷贝赋值运算符
- 移动构造函数
- 移动赋值运算符
3. 零法则(Rule of Zero)
更推荐的做法是:尽量使用std::vector、std::string、std::unique_ptr等自带资源管理的成员,让编译器自动生成所有特殊成员函数。上面的MyClass其实就可以什么都不写,编译器生成的默认移动操作已经足够高效。
4. 对const对象使用std::move无效
conststd::string s="hello";std::string t=std::move(s);// 实际调用的是拷贝构造!因为std::move(s)得到的是const std::string&&,无法绑定到移动构造函数的std::string&&,于是回退到拷贝。
5. 返回局部变量时不要写std::move
std::vector<int>make(){std::vector<int>v(1000);returnv;// 推荐:编译器会做 NRVO 或自动移动// return std::move(v); // 反而可能阻止拷贝省略,不推荐}6. 完美转发(Perfect Forwarding)
与移动语义密切相关的还有万能引用(T&&出现在模板参数推导中)和std::forward,用于在泛型代码中原样保留实参的左值/右值属性,是实现emplace_back、make_unique等函数的基础。
7. 典型应用场景
- 容器扩容时转移元素
- 函数按值返回大对象
- 管理独占资源的类:
std::unique_ptr、std::thread、文件流等(它们只能移动,不能拷贝) - 使用
emplace_back/push_back(std::move(x))向容器添加元素
七、总结
| 概念 | 一句话理解 |
|---|---|
| 移动语义 | 转移资源而非复制,提升性能 |
右值引用&& | 绑定临时对象的引用类型 |
| 移动构造/赋值 | 实现资源"偷取"的特殊成员函数 |
std::move | 只做类型转换,表示"允许被移走" |
noexcept | 让标准库放心使用移动而不是拷贝 |
掌握移动语义,是写出高性能现代 C++(C++11 及以后)代码的关键一步。