移动赋值运算符仅在右侧为右值且左侧支持移动语义时调用,需声明为t& operator=(t&&) noexcept;未定义或未加noexcept会导致退化为深拷贝,影响性能。

移动赋值运算符什么时候真正起作用
它只在右侧对象是右值(比如临时对象、std::move() 转换后的对象)且左侧对象支持移动语义时才被调用。如果你写了 operator= 但没加 && 参数,编译器根本不会选它;如果类里有指针或动态资源,又没写移动赋值,就会退化成深拷贝——性能掉得明显。
- 常见错误现象:
std::vector<bigobject> v1, v2; v1 = std::move(v2);</bigobject>后v2.size()还是非零,说明移动赋值没生效(可能漏了noexcept或没清空原对象) - 必须显式声明为
T& operator=(T&& other) noexcept,否则编译器可能不优先选它 - 典型使用场景:容器扩容、函数返回局部对象、
std::swap内部实现
怎么写一个安全的移动赋值运算符
核心是“掏空”源对象,同时保证目标对象资源可析构。不是简单地把指针赋过去就完事——万一源对象之后被析构,目标对象就悬空了。
- 先检查自赋值:
if (this == &other) return *this;(虽然右值自赋值极少,但保险起见) - 释放当前资源(如
delete[] data;),再接管对方资源:data = other.data; size = other.size; - 立刻置空源对象:
other.data = nullptr; other.size = 0;,否则其析构函数会二次释放 - 务必加
noexcept,否则std::vector在扩容时可能拒绝使用移动而降级为拷贝
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] data;
data = other.data;
size = other.size;
other.data = nullptr;
other.size = 0;
}
return *this;
}
为什么移动赋值后源对象还能用
标准只要求移动后的对象处于“有效但未指定状态”,意思是能安全析构、能赋新值、能调用 clear() 这类不依赖内部状态的函数,但不能假设它还保留原数据。很多初学者误以为移动后源对象一定为空,其实不一定——比如 std::unique_ptr 移动后确实变空,但自定义类型可以按需设计。
- 容易踩的坑:在移动赋值后直接访问
other.size或解引用other.data,行为未定义 - 兼容性影响:若你写的类要放进
std::vector,且移动赋值没加noexcept,容器在扩容时可能强制用拷贝,导致 O(n²) 复杂度 - 参数差异:
operator=(const T&)是拷贝赋值,operator=(T&&)才是移动赋值;两者必须共存,否则右值会退化到拷贝
编译器没调用你的移动赋值?先查这三件事
最常见的不是代码写错,而是调用链里某处悄悄把右值转成了左值,或者移动操作被优化掉了。
立即学习“C++免费学习笔记(深入)”;
- 确认传入的是纯右值:
func(std::move(x))比func(x)更可能触发移动;但若func参数是const T&,那连移动构造都不会进 - 检查类是否隐式删除了移动操作:只要手动写了拷贝构造/拷贝赋值/析构函数中的任一个,编译器就不会自动生成移动成员(C++11 默认行为)
- 看编译器警告:
-Wpessimizing-move(Clang)或/Wall(MSVC)能提示“本该移动却用了拷贝”的情况
移动语义不是银弹,真正省时间的地方是大块内存、文件句柄、网络连接这类不可复制或复制代价高的资源。小结构体加移动赋值反而可能因额外判断拖慢速度。










