移动构造函数必须显式定义,否则编译器不自动生成;若已定义拷贝构造或拷贝赋值,移动构造将被禁用,导致std::move仍触发拷贝;需显式声明并定义移动构造与移动赋值函数,均应标注noexcept,正确转移资源并置空原对象,避免double-free和异常安全问题。

移动构造函数必须显式定义,否则编译器不会自动生成
当你写了拷贝构造函数或拷贝赋值运算符,编译器就不再为你合成移动构造函数——这是最容易被忽略的坑。即使你只写了 MyVector(const MyVector& other),MyVector(MyVector&&) 也不会自动出现,导致 std::move(v) 后调用的仍是拷贝构造,完全没走移动语义。
正确做法是显式声明并定义移动构造函数,把原对象的指针、大小、容量直接“偷过来”,再把原对象置为安全空状态(比如设 m_data = nullptr):
MyVector(MyVector&& other) noexcept
: m_data(other.m_data), m_size(other.m_size), m_capacity(other.m_capacity) {
other.m_data = nullptr;
other.m_size = other.m_capacity = 0;
}
- 务必加
noexcept:否则std::vector在扩容时可能拒绝调用它,退回到拷贝 - 成员初始化列表里直接转移裸指针,别用
std::move包裹内置类型(如m_size),那只是多余赋值 - 移动后原对象必须可析构且不重复释放内存——所以
m_data必须置空
移动赋值运算符要处理自赋值 + 释放旧资源
和移动构造不同,移动赋值得先清理当前持有的资源,再接管对方的。如果没检查自赋值(this == &other),又没做异常安全处理,就可能 double-free 或悬空指针。
推荐写法是“复制-交换”风格,但更轻量的做法是手动释放 + 转移:
MyVector& operator=(MyVector&& other) noexcept {
if (this != &other) {
delete[] m_data;
m_data = other.m_data;
m_size = other.m_size;
m_capacity = other.m_capacity;
other.m_data = nullptr;
other.m_size = other.m_capacity = 0;
}
return *this;
}
- 必须判断
this != &other:否则delete[] m_data会把刚转移来的指针干掉 - 不能漏掉
noexcept:影响容器在std::vector中的移动行为 - 别在移动赋值里调用
clear()或reserve(0):它们可能抛异常,破坏noexcept承诺
移动语义生效的前提:所有资源管理操作都得是 noexcept
哪怕只有一个成员函数(比如析构函数、push_back)可能抛异常,整个类的移动操作就可能被标准库降级为拷贝——因为 std::vector 扩容时要求强异常安全,而移动只有在 noexcept 时才被视为“安全可移动”。
- 析构函数默认是
noexcept,但如果你手动写了带throw()或noexcept(false)的版本,就得改回来 -
operator=和移动构造函数必须显式标注noexcept,不能依赖隐式推导 - 避免在移动路径中调用可能抛异常的函数,例如
std::allocator_traits::allocate不应在此处调用(移动过程不该分配新内存)
测试是否真走了移动:看析构次数和指针值
光看代码不保险。最直接的验证方式是加日志或断点,在移动构造/赋值里打印 m_data 地址,并在析构里计数:
MyVector(MyVector&& other) noexcept {
std::cout
- 用
MyVector v1; MyVector v2 = std::move(v1);触发移动,观察是否只析构一次m_data(即v1的析构不释放内存) - 若看到两次
delete[]同一地址,说明移动后没置空m_data,或者移动赋值没判自赋值 - 用
std::vector测试容器内移动,确认没触发拷贝vec; vec.emplace_back(); vec.push_back(std::move(v));
移动语义不是加了两个函数就自动生效的,每个环节的 noexcept、空状态维护、资源独占性,都得对得上。稍有疏忽,运行时表现和预期就差一个拷贝的开销——而这个开销,在频繁移动大数组时就是性能瓶颈本身。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











