移动构造函数和移动赋值运算符必须显式定义或=default,否则编译器不自动生成;需用T&&参数、noexcept修饰,并确保移动后源对象处于有效但未指定状态。

移动构造函数和移动赋值运算符必须显式定义
编译器不会自动生成移动操作,除非你显式声明或默认它们。即使类里只有 std::vector 这类自带移动语义的成员,若你写了拷贝构造函数或析构函数,编译器就会抑制移动操作的隐式生成——这是最容易被忽略的陷阱。
实操建议:
- 用
= default显式启用编译器生成的移动操作(前提是所有成员都可移动) - 若需自定义逻辑(比如释放资源、更新计数器),必须手写移动构造函数和移动赋值运算符
- 移动赋值运算符要检查自赋值,且必须先清理当前对象状态,再“窃取”源对象资源
右值引用参数必须用 T&&,不能写成 T& 或 const T&
const T& 是万能引用?不,它只能绑定右值但无法修改源对象,也就没法“搬走”资源;T& 根本不能绑定临时对象。真正实现移动的唯一入口是 T&&。
常见错误现象:
立即学习“C++免费学习笔记(深入)”;
- 把移动构造函数写成
MyClass(const MyClass& other)→ 实际调用的是拷贝构造,没走移动 - 返回局部对象时,编译器可能做 RVO 优化,掩盖了移动未生效的问题;加
std::move()强制触发反而破坏 RVO
示例:正确签名长这样
MyClass(MyClass&& other) noexcept;<br>MyClass& operator=(MyClass&& other) noexcept;
noexcept 不是可选修饰,而是移动安全的必要条件
STL 容器(如 std::vector::resize)在重新分配内存时,会优先尝试移动元素;但如果移动操作没标记 noexcept,容器为保异常安全,会退回到拷贝——性能直接掉一档。
为什么必须加:
- 标准库通过
std::is_nothrow_move_constructible_v<t></t>判断能否放心移动 - 自定义移动函数里只要不做可能抛异常的操作(比如 new 失败、文件 I/O),就该加
noexcept - 若成员移动可能抛异常,你得自己处理:要么保证不抛,要么接受容器降级为拷贝
移动后源对象必须处于有效但未指定状态
这不是“随便清空就行”,而是 C++ 标准明确要求:移动后的对象仍可安全析构、可赋值、可再次移动,但值不可预测。很多 bug 来自对这个状态的误判。
容易踩的坑:
- 移动构造函数里把指针置为
nullptr是 OK 的;但若忘了置空,后续析构时 double-delete - 移动赋值中没先释放当前资源,导致内存泄漏
- 认为“移动完源对象就废了”,在移动后还读取其
size()或访问成员变量——行为未定义
一个典型模式:
MyClass& MyClass::operator=(MyClass&& other) noexcept {<br> if (this != &other) {<br> delete[] data_; // 先清理自己<br> data_ = other.data_; // 窃取资源<br> size_ = other.size_;<br> other.data_ = nullptr; // 源对象进入有效状态<br> other.size_ = 0;<br> }<br> return *this;<br>}
移动语义不是“写得炫酷就行”,关键是让每个移动操作都经得起容器重排、异常路径、多次使用这些真实场景的检验。











