移动语义与std::move
需要预先学习:
1. 移动语义
移动语义(Move Semantics)是 C++11 引入的一项重要特性,它的核心思想是:当你把一个对象的资源"转移"到另一个对象时,不需要做深拷贝,而是直接把指针/句柄"偷"过来,让原对象变成一个空壳。这样可以避免大量不必要的拷贝开销,大幅提升性能。
1.1 拷贝的缺点
在传统 C++ 中,对象的传递依赖拷贝。对于管理堆内存的类(如 std::string、std::vector),拷贝意味着:
- 分配新的堆内存
- 逐字节复制原对象的数据
- 原对象和目标对象各自拥有独立的内存
#include <cstring>
#include <iostream>
class MyString
{
public:
MyString(const char* str)
{
_size = std::strlen(str);
_data = new char[_size + 1];
std::strcpy(_data, str);
std::cout << "构造: " << _data << '\n';
}
// 拷贝构造 — 深拷贝,开销大
MyString(const MyString& other)
{
_size = other._size;
_data = new char[_size + 1];
std::strcpy(_data, other._data);
std::cout << "拷贝构造: " << _data << '\n';
}
~MyString()
{
std::cout << "析构: " << (_data ? _data : "null") << '\n';
delete[] _data;
}
private:
char* _data = nullptr;
size_t _size = 0;
};
int main()
{
MyString a("hello"); // 构造
MyString b(a); // 拷贝构造 — 触发深拷贝
return 0;
}
每次拷贝都要 new + strcpy,当字符串很长或拷贝频繁时,性能开销非常明显。
更糟糕的是临时对象的场景:
MyString getGreeting()
{
MyString s("hello world"); // 构造
return s; // 返回时可能触发拷贝
}
int main()
{
MyString msg = getGreeting(); // 又一次拷贝
return 0;
}
临时对象构造完就要销毁,拷贝它的数据是一种浪费——与其"抄一份再烧掉原稿",不如直接把原稿递过去。
1.2 右值引用传参
右值引用用 T&& 表示,它只能绑定到右值(临时对象、字面量、std::move 的返回值等)。
void print(const std::string& s) // 左值引用 — 可以接受左值和右值
{
std::cout << "左值引用版本: " << s << '\n';
}
void print(std::string&& s) // 右值引用 — 只接受右值
{
std::cout << "右值引用版本: " << s << '\n';
}
int main()
{
std::string name = "Alice";
print(name); // 调用左值引用版本
print("Bob"); // 调用右值引用版本(字面量是右值)
print(name + " Smith"); // 调用右值引用版本(表达式结果是临时对象)
return 0;
}
编译并运行代码后,看到这样的输出:
左值引用版本: Alice
右值引用版本: Bob
右值引用版本: Alice Smith
右值引用有两个区别于 const T& 的关键特征:
- 可以修改 — 右值引用指向的对象即将被销毁,允许安全地修改它的内容,避免不必要的拷贝
- 语义明确 — 函数重载时,编译器能区分左值和右值,从而选择不同的实现
void process(std::string&& s)
{
s += " (modified)"; // 可以修改,因为右值即将销毁
std::cout << s << '\n';
}
1.3 移动语义的实现
在类中实现移动语义,需要定义移动构造函数和移动赋值运算符,它们接受右值引用参数,"窃取"资源后将原对象置于安全状态:
#include <cstring>
#include <iostream>
#include <utility> // std::move
class MyString
{
public:
MyString(const char* str)
{
_size = std::strlen(str);
_data = new char[_size + 1];
std::strcpy(_data, str);
std::cout << "构造: " << _data << '\n';
}
// 拷贝构造 — 深拷贝
MyString(const MyString& other)
{
_size = other._size;
_data = new char[_size + 1];
std::strcpy(_data, other._data);
std::cout << "拷贝构造: " << _data << '\n';
}
// 移动构造 — "偷"资源
MyString(MyString&& other) noexcept
: _data(other._data), _size(other._size)
{
other._data = nullptr; // 将原对象置空,防止析构时 double free
other._size = 0;
std::cout << "移动构造: " << _data << '\n';
}
// 移动赋值运算符 — "偷"资源
MyString& operator=(MyString&& other) noexcept
{
if (this != &other)
{
delete[] _data; // 先释放自己的旧资源
_data = other._data; // 接管对方的资源
_size = other._size;
other._data = nullptr; // 将原对象置空
other._size = 0;
}
std::cout << "移动赋值: " << _data << '\n';
return *this;
}
~MyString()
{
std::cout << "析构: " << (_data ? _data : "null") << '\n';
delete[] _data;
}
private:
char* _data = nullptr;
size_t _size = 0;
};
int main()
{
MyString a("hello");
MyString b(std::move(a)); // 移动构造 — a 的资源被"偷"走
// 此时 a._data == nullptr,a 处于"有效但未指定"状态
MyString c("world");
c = std::move(b); // 移动赋值 — c 释放旧资源,接管 b 的资源
return 0;
}
编译并运行代码后,看到这样的输出:
构造: hello
移动构造: hello
构造: world
移动赋值: hello
析构: hello
析构: null
析构: null
移动的核心操作只有三步:
- 接管指针 — 直接把对方的
_data拷过来(浅拷贝) - 接管元数据 — 同步
_size等信息 - 置空原对象 — 把对方的
_data设为nullptr,防止析构时释放已被转移的内存
注意:被移动后的对象处于"有效但未指定"状态——你可以安全地析构它,或给它赋新值,但不应该再访问它原有的内容。
2. 右值的划分与 std::move
在深入 std::move 之前,需要先理清 C++ 中右值的分类体系。
2.1 纯右值与将亡值
C++11 将表达式按"身份"(identity)和"可移动性"(movability)分为以下类别:
![[左值、纯右值与将亡值]](/img/cpp/2.png)
| 类别 | 有身份(可寻址) | 可移动 | 典型例子 |
|---|---|---|---|
| lvalue | 是 | 否 | 变量名、解引用指针 |
| xvalue | 是 | 是 | std::move(a) 的返回值 |
| prvalue | 否 | 是 | 字面量 42、临时对象 std::string("hi") |
- glvalue(generalized lvalue) = lvalue + xvalue — 有身份(有地址、有名字)
- rvalue = prvalue + xvalue — 可移动(可以被"偷"资源)
- lvalue:有身份但不可移动 — 普通变量
- prvalue(pure rvalue,纯右值):无身份,是"纯粹的临时值",如字面量、函数返回的非引用对象
- xvalue(eXpiring value,将亡值):有身份但即将消亡 — 被
std::move标记的左值对象、返回右值引用的函数调用
int a = 10;
int&& r = std::move(a); // r 是左值(有名字!),但它的类型是右值引用
// std::move(a) 这个表达式本身是 xvalue
2.2 std::move 的作用
std::move 本身不做任何移动操作。它只是一个类型转换:无条件地将左值转换为右值引用。
template<typename T>
typename std::remove_reference<T>::type&& move(T&& t) noexcept
{
return static_cast<typename std::remove_reference<T>::type&&>(t);
}
它的作用就是一个"表态"——告诉编译器:“这个变量我不再需要了,你可以把它当作右值来处理”。
#include <utility>
#include <iostream>
#include <string>
int main()
{
std::string s1 = "Hello";
std::string s2 = s1; // 拷贝 — s1 保持不变
std::cout << "s1: " << s1 << '\n'; // "Hello"
std::string s3 = std::move(s1); // 移动 — s1 的资源被转移到 s3
std::cout << "s1: " << s1 << '\n'; // 空或未指定内容
std::cout << "s3: " << s3 << '\n'; // "Hello"
return 0;
}
典型使用场景:
-
向容器中放入大对象:
std::vector<std::string> vec; std::string big = "a very long string..."; vec.push_back(std::move(big)); // 移动而非拷贝 -
从函数返回局部对象时不需要 std::move — 编译器会自动使用移动:
std::string createString() { std::string result = "hello"; return result; // 自动移动(NRVO/移动构造) // 不要写 return std::move(result); — 会抑制 NRVO 优化 } -
unique_ptr 的所有权转移:
std::unique_ptr<Foo> p1 = std::make_unique<Foo>(); std::unique_ptr<Foo> p2 = std::move(p1); // p1 变为 nullptr,p2 接管所有权
3. 移动构造函数与移动赋值运算符
当一个类管理资源(堆内存、文件句柄、网络连接等)时,应该实现移动构造函数和移动赋值运算符,这是 Rule of Five 的组成部分。
class Buffer
{
public:
// 1. 构造函数
Buffer(size_t size) : _size(size), _data(new int[size]) {}
// 2. 析构函数
~Buffer() { delete[] _data; }
// 3. 拷贝构造函数
Buffer(const Buffer& other) : _size(other._size), _data(new int[other._size])
{
std::copy(other._data, other._data + _size, _data);
}
// 4. 拷贝赋值运算符
Buffer& operator=(const Buffer& other)
{
if (this != &other)
{
delete[] _data;
_size = other._size;
_data = new int[_size];
std::copy(other._data, other._data + _size, _data);
}
return *this;
}
// 5. 移动构造函数 — noexcept 很重要!
Buffer(Buffer&& other) noexcept
: _size(other._size), _data(other._data)
{
other._data = nullptr;
other._size = 0;
}
// 6. 移动赋值运算符 — noexcept 很重要!
Buffer& operator=(Buffer&& other) noexcept
{
if (this != &other)
{
delete[] _data;
_data = other._data;
_size = other._size;
other._data = nullptr;
other._size = 0;
}
return *this;
}
private:
size_t _size = 0;
int* _data = nullptr;
};
为什么移动操作要加 noexcept?
std::vector 在扩容时会搬移元素。如果元素的移动构造函数有 noexcept 标记,vector 会安心使用移动(性能高);如果没有 noexcept,为了"强异常安全保证",vector 会退化到拷贝(性能低)。
static_assert(std::is_nothrow_move_constructible<Buffer>::value,
"Buffer 的移动构造应该是 noexcept 的");


