移动语义是C++11引入的一项重要特性,用来提升性能并减少不必要的复制。它让我们能够在对象生命周期内安全地“转移”资源,而不是复制。下面从概念、实现、使用场景以及常见陷阱四个角度,系统性地讲解移动语义的核心内容,并给出实用代码示例。
1. 移动语义的核心思想
- 复制:
T b = a;需要把a的内部状态逐个拷贝到b。如果T持有大型资源(如动态数组、文件句柄),复制成本高且可能导致不必要的拷贝构造/析构调用。 - 移动:
T b = std::move(a);把a的资源指针或句柄直接转移给b,a被置为安全的空状态。无需逐个拷贝,成本仅为指针赋值。
安全空状态:任何对象在移动后都必须能安全地析构。最常见做法是把指针置为
nullptr,长度置为。
2. 移动构造函数与移动赋值运算符
class Buffer {
public:
Buffer(size_t n) : data(new int[n]), sz(n) {}
~Buffer() { delete[] data; }
// 禁止拷贝
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
// 移动构造
Buffer(Buffer&& other) noexcept
: data(other.data), sz(other.sz) {
other.data = nullptr;
other.sz = 0;
}
// 移动赋值
Buffer& operator=(Buffer&& other) noexcept {
if (this != &other) {
delete[] data; // 释放旧资源
data = other.data; // 转移资源
sz = other.sz;
other.data = nullptr; // 把对方置为空
other.sz = 0;
}
return *this;
}
private:
int* data;
size_t sz;
};
noexcept声明:移动操作不抛异常,方便标准库容器使用std::vector时做优化。- 删除拷贝构造和赋值运算符:防止意外复制。
3. 标准库中的移动语义
std::vector:在扩容时会移动元素而不是复制(如果元素支持移动)。std::unique_ptr:实现了移动构造和移动赋值,禁止复制。通过std::move可以把所有权转移给另一个unique_ptr。std::string:C++17 起采用“小字符串优化”(SSO),对小字符串使用栈存储,大字符串使用堆。移动时仅移动堆指针,SSE 字符串保持栈状态,提升效率。
4. 何时使用移动
| 场景 | 说明 |
|---|---|
| 返回大对象 | 在函数返回值时,使用 return Buffer{n}; 会触发移动而不是复制(NRVO 或移动构造)。 |
| 容器中插入 | vec.push_back(std::move(obj)); 将对象的资源搬迁到容器。 |
| 临时对象 | auto f = [](int){ return Buffer{100}; }; auto b = f(0); 直接使用移动构造。 |
| 资源所有权转移 | 如文件句柄、网络连接等,用 std::unique_ptr 携带自定义删除器,利用移动转移所有权。 |
5. 常见陷阱与注意事项
-
忘记
noexcept
若移动构造/赋值不是noexcept,std::vector在扩容时会退回到拷贝,导致性能下降。// 错误写法 Buffer(Buffer&& other); // 未标记 noexcept -
未正确置空
移动后对象若未置空,析构时会双重释放。Buffer(Buffer&& other) : data(other.data), sz(other.sz) { other.data = nullptr; // 必须 } -
误用
std::move
对于已是右值的对象再std::move没意义,但对左值一定要std::move,否则会走拷贝。Buffer b1{100}; Buffer b2 = std::move(b1); // 必须 -
移动后对象的状态
移动后对象应保持可析构、可复制(如果支持)或至少可移动。不要在移动后立即访问其内部数据。 -
移动构造不应调用
delete
移动构造只负责转移资源指针,不要释放旧资源(旧资源已被转移)。
6. 代码演示:智能容器与移动
#include <iostream>
#include <vector>
#include <memory>
class Widget {
public:
Widget(int id) : id_(id) { std::cout << "Widget(" << id_ << ") ctor\n"; }
~Widget() { std::cout << "Widget(" << id_ << ") dtor\n"; }
Widget(const Widget&) = delete; // 禁止拷贝
Widget& operator=(const Widget&) = delete;
Widget(Widget&& other) noexcept : id_(other.id_) {
std::cout << "Widget(" << id_ << ") move ctor\n";
other.id_ = -1; // 置空
}
Widget& operator=(Widget&& other) noexcept {
if (this != &other) {
std::cout << "Widget(" << id_ << ") move assign\n";
id_ = other.id_;
other.id_ = -1;
}
return *this;
}
private:
int id_;
};
int main() {
std::vector<std::unique_ptr<Widget>> v;
for (int i = 0; i < 3; ++i) {
v.push_back(std::make_unique <Widget>(i));
}
// 移动 Widget
auto w = std::make_unique <Widget>(10);
v.push_back(std::move(w)); // w 现在为空
std::cout << "Vector size: " << v.size() << '\n';
}
运行结果示例(简化):
Widget(0) ctor
Widget(1) ctor
Widget(2) ctor
Widget(10) ctor
Widget(0) dtor
Widget(1) dtor
Widget(2) dtor
Widget(10) dtor
观察到没有出现拷贝构造,只是移动。
7. 结语
移动语义让 C++ 在性能和内存管理上更上一层楼。掌握它不仅能写出更高效的代码,还能让你在使用 STL 和第三方库时得到更好的体验。记住:移动=转移资源,保持空状态;不要忘记 noexcept,避免不必要的拷贝。 Happy coding!