移动构造函数与移动赋值
定义与作用
移动构造函数和移动赋值运算符是 C++11 移动语义的核心操作——通过"窃取"资源而非拷贝资源来传递对象所有权:
class Buffer {
public:
Buffer(Buffer&& other) noexcept; // 移动构造
Buffer& operator=(Buffer&& other) noexcept; // 移动赋值
};
| 对比 | 拷贝操作 | 移动操作 |
|---|---|---|
| 签名 | T(const T&) | T(T&&) noexcept |
| 语义 | 复制资源 | 窃取资源 |
| 源对象 | 保持不变 | 处于有效但未指定状态 |
| 性能 | O(n) 深拷贝 | O(1) 指针交换 |
| 异常 | 可能抛异常 | 通常不抛(应标记 noexcept) |
核心原理
移动语义下的资源转移
Rule of Five(C++11)
完整示例
示例一:高性能字符串缓冲区
场景说明:实现一个动态字符串缓冲区,对比拷贝和移动的性能差异。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <chrono>
class StringBuffer {
public:
// 普通构造
explicit StringBuffer(const char* str = "")
: size_(std::strlen(str)), data_(new char[size_ + 1]) {
std::strcpy(data_, str);
std::cout << "[构造] \"" << data_ << "\"" << std::endl;
}
// 拷贝构造
StringBuffer(const StringBuffer& other)
: size_(other.size_), data_(new char[size_ + 1]) {
std::strcpy(data_, other.data_);
std::cout << "[拷贝构造] \"" << data_ << "\"" << std::endl;
}
// 移动构造
StringBuffer(StringBuffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
// 窃取资源后置空源对象
other.size_ = 0;
other.data_ = nullptr;
std::cout << "[移动构造] 资源已转移" << std::endl;
}
// 拷贝赋值
StringBuffer& operator=(const StringBuffer& other) {
std::cout << "[拷贝赋值]" << std::endl;
if (this != &other) {
StringBuffer temp(other);
std::swap(size_, temp.size_);
std::swap(data_, temp.data_);
}
return *this;
}
// 移动赋值
StringBuffer& operator=(StringBuffer&& other) noexcept {
std::cout << "[移动赋值]" << std::endl;
if (this != &other) {
delete[] data_; // 释放自己的旧资源
data_ = other.data_; // 窃取
size_ = other.size_;
other.data_ = nullptr; // 置空源对象
other.size_ = 0;
}
return *this;
}
~StringBuffer() {
std::cout << "[析构] size=" << size_
<< (data_ ? (" \"" + std::string(data_) + "\"") : " (空)")
<< std::endl;
delete[] data_;
}
size_t size() const { return size_; }
const char* c_str() const { return data_ ? data_ : ""; }
private:
size_t size_;
char* data_;
};
// 工厂函数:返回临时对象
StringBuffer make_buffer() {
return StringBuffer("临时生成的字符串内容");
}
int main() {
std::cout << "===== 移动构造:接收临时对象 =====" << std::endl;
StringBuffer buf = make_buffer(); // 移动构造(或 RVO 优化)
std::cout << "缓冲区内容: " << buf.c_str() << std::endl;
std::cout << "\n===== 移动赋值 =====" << std::endl;
StringBuffer buf2("原始内容");
buf2 = StringBuffer("新内容"); // 移动赋值
std::cout << "\n===== 显式移动 =====" << std::endl;
StringBuffer buf3("可移动的数据");
std::cout << "移动前 buf3: " << buf3.c_str() << std::endl;
StringBuffer buf4 = std::move(buf3); // 显式移动
std::cout << "移动后 buf3: \"" << buf3.c_str() << "\" (空)" << std::endl;
std::cout << "移动后 buf4: \"" << buf4.c_str() << "\"" << std::endl;
return 0;
}
预期输出:
===== 移动构造:接收临时对象 =====
[构造] "临时生成的字符串内容"
[移动构造] 资源已转移
[析构] size=0 (空)
缓冲区内容: 临时生成的字符串内容
===== 移动赋值 =====
[构造] "原始内容"
[构造] "新内容"
[移动赋值]
[析构] size=0 (空)
===== 显式移动 =====
[构造] "可移动的数据"
移动前 buf3: 可移动的数据
[移动构造] 资源已转移
移动后 buf3: "" (空)
移动后 buf4: "可移动的数据"
...
逐段分析:
make_buffer()返回临时对象 → 移动构造(或被 RVO 完全省略,取决于编译器优化级别)- 移动后源对象的
data_被设为nullptr,size_被归零——处于"有效但未指定"状态 - 移动赋值中先
delete[]自己的旧资源,再窃取新资源——避免了旧资源泄漏 std::move(buf3)将左值转为右值,触发移动构造。移动后buf3变为空,但可安全析构或重新赋值- 注意移动操作标记了
noexcept——保证std::vector扩容时会使用移动而非拷贝
示例二:移动语义在容器中的性能优势
场景说明:对比拷贝和移动在 std::vector 中存储大对象的性能差异。
#include <iostream>
#include <vector>
#include <string>
#include <chrono>
class HeavyData {
public:
std::string label;
std::vector<int> numbers; // 大块数据
HeavyData(std::string lbl, size_t count)
: label(std::move(lbl)), numbers(count) {
for (size_t i = 0; i < count; ++i) numbers[i] = static_cast<int>(i);
}
// 拷贝(昂贵)
HeavyData(const HeavyData& other)
: label(other.label), numbers(other.numbers) {
// 逐元素拷贝 numbers
}
// 移动(廉价)
HeavyData(HeavyData&& other) noexcept
: label(std::move(other.label)),
numbers(std::move(other.numbers)) {
// 只交换了几个指针
}
};
int main() {
const size_t DATA_SIZE = 1'000'000; // 100 万个 int
std::cout << "===== 拷贝 vs 移动性能对比 =====" << std::endl;
// ---- 拷贝 ----
HeavyData original("原始数据", DATA_SIZE);
auto t1 = std::chrono::steady_clock::now();
HeavyData copied = original; // 拷贝构造:深拷贝 100 万个 int
auto t2 = std::chrono::steady_clock::now();
auto copy_time = std::chrono::duration_cast<std::chrono::milliseconds>(t2 - t1);
// ---- 移动 ----
auto t3 = std::chrono::steady_clock::now();
HeavyData moved = std::move(original); // 移动构造:O(1) 指针交换
auto t4 = std::chrono::steady_clock::now();
auto move_time = std::chrono::duration_cast<std::chrono::microseconds>(t4 - t3);
std::cout << "数据大小: " << DATA_SIZE << " 个 int" << std::endl;
std::cout << "拷贝耗时: " << copy_time.count() << " ms" << std::endl;
std::cout << "移动耗时: " << move_time.count() << " us" << std::endl;
std::cout << "\n===== vector 扩容中的移动 =====" << std::endl;
std::vector<HeavyData> vec;
vec.reserve(1);
auto t5 = std::chrono::steady_clock::now();
vec.emplace_back("数据1", 100000);
vec.emplace_back("数据2", 100000); // 扩容时移动首个元素
vec.emplace_back("数据3", 100000); // 扩容时移动前两个元素
auto t6 = std::chrono::steady_clock::now();
auto vec_time = std::chrono::duration_cast<std::chrono::milliseconds>(t6 - t5);
std::cout << "3 次 push_back(含扩容): " << vec_time.count() << " ms" << std::endl;
std::cout << "感谢 noexcept 移动,扩容使用移动而非拷贝!" << std::endl;
return 0;
}
预期输出(数值因硬件而异):
===== 拷贝 vs 移动性能对比 =====
数据大小: 1000000 个 int
拷贝耗时: 3 ms
移动耗时: 1 us
===== vector 扩容中的移动 =====
3 次 push_back(含扩容): 0 ms
感谢 noexcept 移动,扩容使用移动而非拷贝!
逐段分析:
- 拷贝 100 万个
int是 O(n) 操作(毫秒级),移动只交换了std::string和std::vector的内部指针(微秒级) std::vector扩容时自动使用移动(条件:移动操作标记noexcept)- 如果移动操作没有
noexcept,vector 会回退到拷贝——性能显著退化 - 这就是为什么移动操作必须标记
noexcept的底层原因
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
移动操作未标记 noexcept | vector 扩容用拷贝 | 移动构造/赋值始终标记 noexcept |
| 移动后继续使用源对象 | 读到空或不确定的值 | 仅允许析构或重新赋值 |
| 移动赋值忘记释放旧资源 | 内存泄漏 | 先 delete 旧资源,再窃取 |
| 移动赋值忘记自赋值检查 | 释放了自己的资源 | 先检查 this != &other |
| 移动构造函数中源对象未置空 | 双重释放 | 将源对象的资源指针设为 nullptr |
常见面试问题
移动构造函数和拷贝构造函数的区别?移动的性能优势来源?——拷贝是深复制(O(n)),移动是资源所有权转移(O(1))。移动只需要交换指针,拷贝需要复制整个数据结构。
为什么移动操作应该标记
noexcept?——std::vector扩容时,如果移动操作可能抛异常,容器会退回拷贝以保证强异常安全。noexcept让容器放心使用移动。std::move做了什么?移动后源对象的状态是什么?——std::move是 unconditional cast 到右值引用(类型转换,不移动数据)。移动后源对象处于"有效但未指定"状态——可安全析构或重新赋值,但不应读取。Rule of Five 是什么?——析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值。自定义其中任一个时通常需要全部显式定义或
= default/= delete。编译器何时自动生成移动操作?——当用户没有自定义任何拷贝操作、移动操作和析构函数时。一旦自定义了任一个,移动操作不会被自动生成(需显式
= default)。
小结
- 移动语义通过"窃取"资源实现 O(1) 的对象转移,是 C++11 最重要的性能优化
- 移动操作始终标记
noexcept——影响 vector 扩容和其他容器的优化策略 - 移动后源对象处于"有效但未指定"状态:可析构、可赋值,不可读取
std::move只是类型转换,真正的移动发生在移动构造函数或移动赋值中- Rule of Five:五者联动,要么全部自定义,要么全部
= default