一、为什么要引入移动语义
1. 拷贝的开销
假设我们自己实现一个简单的动态数组:
cpp
#include <iostream>
#include <vector>
class Buffer {
size_t size;
char* data;
public:
Buffer(size_t n) : size(n), data(new char[n]) {}
Buffer(size_t n, const char* src) : size(n), data(new char[n]) {
std::copy(src, src + n, data);
}
~Buffer() { delete[] data; }
// 拷贝构造(深拷贝)
Buffer(const Buffer& other) : size(other.size), data(new char[other.size]) {
std::copy(other.data, other.data + size, data);
}
const size_t getSize() const { return size; }
const char* getData() const { return data; }
// 拷贝赋值
Buffer& operator=(const Buffer& other) {
if (this != &other) {
delete[] data;
size = other.size;
data = new char[size];
std::copy(other.data, other.data + size, data);
}
return *this;
}
};
int main()
{
size_t size = 12;
char* c = new char[size];
for (size_t i = 0; i < size; i++)
{
c[i] = '1';
}
Buffer b(size, c);
delete[] c;
for (size_t i = 0; i < b.getSize(); i++)
{
std::cout << b.getData()[i] << std::endl;
}
}
一切正常,直到我们写出这样的代码:
cpp
Buffer createBuffer() {
Buffer tmp(1024);
// ... 填充数据
return tmp; // 返回临时对象
}
Buffer buf = createBuffer(); // 可能有一次拷贝构造
在 C++11 之前,createBuffer 返回时通常会发生一次拷贝(尽管有返回值优化 RVO/NRVO,但并非所有场景都能优化)。如果 Buffer 持有 1GB 数据,这次拷贝的代价就非常大。更糟的是,在表达式中使用的临时对象会触发频繁的拷贝:
cpp
#include <iostream>
#include <vector>
#include <chrono>
// 模拟一个管理堆内存的大缓冲区类
class Buffer {
private:
size_t* data; // 模拟大块资源(用 size_t 便于观察)
size_t size;
size_t id; // 对象标识,便于跟踪
static size_t counter; // 全局计数器,用于生成唯一 ID
public:
// 构造函数:分配资源
explicit Buffer(size_t sz)
: size(sz), id(++counter)
{
std::cout << "[构造] Buffer #" << id << " 分配 " << size << " 个元素" << std::endl;
data = new size_t[size];
// 模拟写入数据(实际可能是从网络/磁盘读取)
for (size_t i = 0; i < size; ++i) {
data[i] = i;
}
}
// 拷贝构造函数(昂贵的深拷贝)
Buffer(const Buffer& other)
: size(other.size), id(++counter)
{
std::cout << "[拷贝构造] Buffer #" << other.id << " -> #" << id
<< " (深拷贝 " << size << " 个元素)" << std::endl;
data = new size_t[size];
// 逐元素复制 ------ 当 size 为 1GB 时,代价巨大
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
}
// 拷贝赋值运算符(昂贵的深拷贝)
Buffer& operator=(const Buffer& other) {
std::cout << "[拷贝赋值] Buffer #" << id << " 被赋值为 #" << other.id
<< " (深拷贝 " << other.size << " 个元素)" << std::endl;
if (this != &other) {
delete[] data; // 释放旧资源
size = other.size;
data = new size_t[size];
for (size_t i = 0; i < size; ++i) {
data[i] = other.data[i];
}
// id 保持不变,表示对象身份没变
}
return *this;
}
// ---------- C++11 移动语义 ----------
// 移动构造函数("窃取"资源)
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size), id(++counter)
{
std::cout << "[移动构造] Buffer #" << other.id << " 的资源被移动到 #" << id << std::endl;
// 将源对象置于安全状态
other.data = nullptr;
other.size = 0;
// other.id 保持原样,但已无资源
}
// 移动赋值运算符
Buffer& operator=(Buffer&& other) noexcept {
std::cout << "[移动赋值] Buffer #" << id << " 接管 #" << other.id << " 的资源" << std::endl;
if (this != &other) {
delete[] data; // 释放当前持有的资源
// 直接接管 other 的资源
data = other.data;
size = other.size;
// id 不变
// 置空 other
other.data = nullptr;
other.size = 0;
}
return *this;
}
// 析构函数
~Buffer() {
std::cout << "[析构] Buffer #" << id;
if (data) {
std::cout << " 释放 " << size << " 个元素";
delete[] data;
}
else {
std::cout << " (无资源)";
}
std::cout << std::endl;
}
// 工具函数
size_t getSize() const { return size; }
bool hasData() const { return data != nullptr; }
};
size_t Buffer::counter = 0;
// 模拟创建 Buffer 的工厂函数
Buffer createBuffer(size_t size) {
Buffer tmp(size); // 构造临时对象
// ... 填充数据
return tmp; // 返回临时对象,C++11 起会优先使用移动(或 RVO)
}
// 运行示例
int main() {
std::cout << "===== 1. 返回临时对象(C++11 起移动 / RVO)=====" << std::endl;
{
Buffer buf = createBuffer(10); // 若无移动,可能会深拷贝 10 个元素
std::cout << "buf 持有资源吗? " << (buf.hasData() ? "是" : "否") << std::endl;
}
std::cout << std::endl;
std::cout << "===== 2. 显式拷贝 vs 移动 =====" << std::endl;
{
Buffer a(20);
std::cout << "\n-- 拷贝构造 --" << std::endl;
Buffer b = a; // 深拷贝
std::cout << "a 持有资源吗? " << (a.hasData() ? "是" : "否") << std::endl;
std::cout << "b 持有资源吗? " << (b.hasData() ? "是" : "否") << std::endl;
std::cout << "\n-- 移动构造 --" << std::endl;
Buffer c = std::move(a); // 移动,a 的资源被窃取
std::cout << "a 持有资源吗? " << (a.hasData() ? "是" : "否") << std::endl;
std::cout << "c 持有资源吗? " << (c.hasData() ? "是" : "否") << std::endl;
// 注意:b 和 c 离开作用域时正常释放,a 无资源可释放
}
std::cout << std::endl;
std::cout << "===== 3. 容器插入临时对象(移动)=====" << std::endl;
{
std::vector<Buffer> vec;
std::cout << "插入第一个元素:" << std::endl;
vec.push_back(Buffer(30)); // 临时对象,移动进容器
std::cout << "插入第二个元素:" << std::endl;
vec.push_back(Buffer(40)); // 可能触发扩容,原有元素被移动
std::cout << "vector 大小: " << vec.size() << std::endl;
}
std::cout << std::endl;
std::cout << "===== 4. 容器拷贝 vs 移动元素 =====" << std::endl;
{
std::vector<Buffer> vec1;
vec1.emplace_back(50); // 原地构造
vec1.emplace_back(60);
std::cout << "\n拷贝整个 vector(深拷贝):" << std::endl;
std::vector<Buffer> vec2 = vec1; // 每个元素深拷贝
std::cout << "\n移动整个 vector:" << std::endl;
std::vector<Buffer> vec3 = std::move(vec1); // vec1 的元素被移动到 vec3
std::cout << "vec1 大小: " << vec1.size() << std::endl; // 通常为 0
}
return 0;
}
拷⻉的真正问题不在于"复制字节",而在于必须复制资源本身(分配新内存、复制大量数据),然后立刻销毁旧对象。我们真正想要的只是"把临时对象里面的指针偷过来",而不是再克隆一份。
2. 移动的直觉
"移动"就像一个窃贼:把原对象的资源"窃取"过来,然后让原对象处于一个"空但安全"的状态。这样我们只修改了几个指针或句柄,成本极低。
C++11 引入了 右值引用 (&&)来标识"可以安全移动"的对象(通常是临时对象、即将销毁的对象),从而让编译器和程序员能够区分:
1.Buffer(const Buffer&) ------ 左值引用,用于拷贝。
2.Buffer(Buffer&&) ------ 右值引用,用于移动。
cpp
#include <iostream>
#include <cstring>
#include <vector>
#include <utility> // std::move
// 模拟一个管理堆内存的大资源类(如图像、网络包、大数组)
class GiantBuffer {
private:
char* data; // 指向堆内存的原始指针
size_t size; // 数据大小(字节)
size_t id; // 对象唯一标识,方便追踪
static size_t counter; // 全局对象计数器
public:
// 构造函数:分配资源(就像买了一套昂贵的房子)
explicit GiantBuffer(size_t sizeInBytes)
: size(sizeInBytes), id(++counter)
{
std::cout << "[构造] 对象 #" << id << " 分配了 " << size << " 字节内存。\n";
data = new char[size];
// 模拟填充数据(实际可能从磁盘读取)
std::memset(data, 'A', size);
}
// 拷贝构造函数(左值引用):深拷贝,代价高昂
GiantBuffer(const GiantBuffer& other)
: size(other.size), id(++counter)
{
std::cout << "[拷贝构造] 从对象 #" << other.id
<< " 深拷贝到新对象 #" << id
<< ",复制 " << size << " 字节数据...(非常慢!)\n";
data = new char[size];
std::memcpy(data, other.data, size); // 逐字节复制
}
// 拷贝赋值运算符(左值引用)
GiantBuffer& operator=(const GiantBuffer& other) {
std::cout << "[拷贝赋值] 对象 #" << id
<< " 被赋值为对象 #" << other.id
<< " 的副本(深拷贝)。\n";
if (this != &other) {
delete[] data;
size = other.size;
data = new char[size];
std::memcpy(data, other.data, size);
}
return *this;
}
// ---------- 移动语义:窃贼来了 ----------
// 移动构造函数(右值引用):"窃取"源对象的资源
GiantBuffer(GiantBuffer&& other) noexcept
: data(other.data), size(other.size), id(++counter)
{
std::cout << "[移动构造] 窃贼 #" << id << " 把对象 #" << other.id
<< " 的资源偷走了!(仅窃取指针和大小,0次内存分配)\n";
// 让源对象处于"空但安全"的状态
other.data = nullptr;
other.size = 0;
// other.id 保持不变,它成了一个空壳子
}
// 移动赋值运算符(右值引用)
GiantBuffer& operator=(GiantBuffer&& other) noexcept {
std::cout << "[移动赋值] 对象 #" << id
<< " 直接接管了对象 #" << other.id << " 的资源。\n";
if (this != &other) {
delete[] data; // 释放自己原有的资源
// 直接窃取
data = other.data;
size = other.size;
// 置空源对象
other.data = nullptr;
other.size = 0;
}
return *this;
}
// 析构函数:只有持有资源时才释放
~GiantBuffer() {
std::cout << "[析构] 对象 #" << id;
if (data) {
std::cout << " 释放 " << size << " 字节内存。\n";
delete[] data;
}
else {
std::cout << " 是空壳子,无须释放资源。\n";
}
}
size_t getSize() const { return size; }
bool isEmpty() const { return data == nullptr; }
};
size_t GiantBuffer::counter = 0;
// 工厂函数:返回临时对象(典型的右值)
GiantBuffer createBuffer(size_t bytes) {
GiantBuffer tmp(bytes); // 局部对象
// ... 可能进一步处理 tmp
return tmp; // 返回时优先移动(或RVO)
}
int main() {
std::cout << "===== 场景1:返回临时对象 =====\n";
{
GiantBuffer buf = createBuffer(1024); // 1 KB 示例,若1GB也一样
std::cout << "buf 是空壳吗? " << (buf.isEmpty() ? "是" : "否") << "\n\n";
} // buf 离开作用域,释放资源
std::cout << "===== 场景2:左值拷贝 vs 右值移动 =====\n";
{
GiantBuffer original(2048);
std::cout << "\n-- 左值触发深拷贝 --\n";
GiantBuffer copy = original; // 调用拷贝构造函数
std::cout << "original 是空壳吗? " << (original.isEmpty() ? "是" : "否") << "\n";
std::cout << "copy 是空壳吗? " << (copy.isEmpty() ? "是" : "否") << "\n";
std::cout << "\n-- 右值触发移动(窃取) --\n";
GiantBuffer thief = std::move(original); // 强制转成右值,调用移动构造函数
std::cout << "original 是空壳吗? " << (original.isEmpty() ? "是" : "否")
<< "(被窃取后,安全变空)\n";
std::cout << "thief 是空壳吗? " << (thief.isEmpty() ? "是" : "否") << "\n";
// original 仍能安全析构,因为其 data==nullptr
}
std::cout << "\n";
std::cout << "===== 场景3:容器与临时对象 =====\n";
{
std::vector<GiantBuffer> vec;
std::cout << "往 vector 中压入临时对象(右值),触发移动:\n";
vec.push_back(GiantBuffer(4096)); // 临时对象 → 移动到容器内
std::cout << "vec[0] 是空壳吗? " << (vec[0].isEmpty() ? "是" : "否") << "\n";
std::cout << "\n再压入一个元素,可能引发 vector 扩容:\n";
vec.push_back(GiantBuffer(8192));
// 扩容时,旧元素会被移动(因为移动构造是noexcept的)
std::cout << "扩容完成,vec[0] 是空壳吗? " << (vec[0].isEmpty() ? "是" : "否") << "\n";
std::cout << "vec[1] 是空壳吗? " << (vec[1].isEmpty() ? "是" : "否") << "\n";
}
std::cout << "\n";
std::cout << "===== 场景4:显式移动 vs 拷贝赋值 =====\n";
{
GiantBuffer buf1(512);
GiantBuffer buf2(256);
std::cout << "\n-- 拷贝赋值 --\n";
buf2 = buf1; // buf1 是左值,调用拷贝赋值
std::cout << "buf1 是空壳吗? " << (buf1.isEmpty() ? "是" : "否") << "\n";
std::cout << "buf2 是空壳吗? " << (buf2.isEmpty() ? "是" : "否") << "\n";
std::cout << "\n-- 移动赋值 --\n";
buf2 = std::move(buf1); // 将 buf1 转成右值,移动赋值
std::cout << "buf1 是空壳吗? " << (buf1.isEmpty() ? "是" : "否") << "(资源被窃取)\n";
std::cout << "buf2 是空壳吗? " << (buf2.isEmpty() ? "是" : "否") << "\n";
}
return 0;
}
二、必要的概念:左值、右值、将亡值
要想用好移动语义,必须了解一点值类别。
1.左值 (lvalue):可以取地址、有名字的对象,生命周期较长。如变量 int a = 5; 中的 a。
2.右值 (rvalue):不能取地址,通常是临时对象或字面量。如 5、a + b、函数返回的非引用临时对象。
3.将亡值 (xvalue):C++11 引入的细分概念,指即将被移动的对象,例如 std::move(x) 的结果,或返回右值引用的函数返回值。广义上 xvalue 属于 rvalue。
区分的方法很简单:能对表达式使用 & 取地址的,是左值;否则是右值。
临时对象是右值,因此可以安全地转移其资源,因为再也没有人需要它了。
cpp
#include <iostream>
#include <utility> // std::move
// 一个管理堆内存的类,清晰展示拷贝与移动
class Buffer {
private:
int* data;
size_t size;
size_t id;
static size_t counter;
public:
explicit Buffer(size_t sz)
: size(sz), id(++counter)
{
std::cout << "[构造] Buffer #" << id << " 分配了 " << size << " 个 int\n";
data = new int[size];
for (size_t i = 0; i < size; ++i) data[i] = static_cast<int>(i);
}
// 拷贝构造(左值引用)
Buffer(const Buffer& other)
: size(other.size), id(++counter)
{
std::cout << "[拷贝构造] Buffer #" << other.id << " -> #" << id
<< " (深拷贝 " << size << " 个 int)\n";
data = new int[size];
for (size_t i = 0; i < size; ++i) data[i] = other.data[i];
}
// 移动构造(右值引用)
Buffer(Buffer&& other) noexcept
: data(other.data), size(other.size), id(++counter)
{
std::cout << "[移动构造] Buffer #" << other.id << " -> #" << id
<< " (偷走资源,零拷贝)\n";
other.data = nullptr;
other.size = 0;
}
~Buffer() {
std::cout << "[析构] Buffer #" << id;
if (data) {
std::cout << " 释放 " << size << " 个 int\n";
delete[] data;
}
else {
std::cout << " (空壳)\n";
}
}
// 工具函数
size_t getSize() const { return size; }
bool hasData() const { return data != nullptr; }
};
size_t Buffer::counter = 0;
// ------------------- 值类别探测 -------------------
// 重载函数:根据参数是左值还是右值调用不同版本
void inspect(Buffer& buf) {
std::cout << " [探测] 传入了 左值 (lvalue),不能偷资源\n";
}
void inspect(Buffer&& buf) {
std::cout << " [探测] 传入了 右值 (rvalue),可以安全地偷走资源\n";
}
// 返回右值引用的函数 ------ 产生将亡值 (xvalue)
Buffer&& createXValue() {
static Buffer global(100); // 静态对象,避免返回局部对象的引用
return std::move(global);
}
// 返回临时对象的函数 ------ 产生纯右值 (prvalue)
Buffer createTemp() {
return Buffer(200);
}
int main() {
std::cout << "===== 1. 左值 (lvalue) =====\n";
Buffer a(10); // a 是左值
inspect(a); // 调用 inspect(Buffer&)
// 可以用 &a 取地址
std::cout << "&a = " << &a << "\n\n";
std::cout << "===== 2. 右值 (rvalue):临时对象 =====\n";
inspect(Buffer(20)); // 临时对象,右值,调用 inspect(Buffer&&)
// 不能取地址: &Buffer(20) 编译错误
std::cout << "\n";
std::cout << "===== 3. 右值 (rvalue):表达式结果 =====\n";
Buffer x(5), y(6);
// x + y 没有定义,我们用函数返回临时对象来模拟
inspect(createTemp()); // 函数返回的临时对象,右值
std::cout << "\n";
std::cout << "===== 4. 将亡值 (xvalue):std::move =====\n";
Buffer b(30);
inspect(std::move(b)); // std::move(b) 是将亡值,右值的一种
std::cout << "b 被 std::move 后,资源是否被偷? "
<< (b.hasData() ? "否(尚未被偷)" : "是(已被偷)")
<< " ------ 注意:std::move 本身不移动,只是类型转换\n";
// 真正移动发生在将亡值传递给移动构造或移动赋值时
Buffer c = std::move(b); // 这里调用移动构造,资源被偷
std::cout << "移动构造后,b 有数据吗? " << (b.hasData() ? "有" : "无") << "\n\n";
std::cout << "===== 5. 将亡值 (xvalue):返回右值引用的函数 =====\n";
inspect(createXValue()); // createXValue() 返回 &&,是将亡值
std::cout << "\n";
std::cout << "===== 6. 拷贝 vs 移动的自动选择 =====\n";
Buffer src(40);
std::cout << "\n从左值构造(拷贝):\n";
Buffer copy1 = src; // src 是左值,调用拷贝构造
std::cout << "\n从将亡值构造(移动):\n";
Buffer move1 = std::move(src); // 将亡值,调用移动构造
std::cout << "\n从临时对象构造(移动):\n";
Buffer move2 = createTemp(); // 纯右值,调用移动构造(或RVO)
std::cout << "\n";
std::cout << "===== 7. 总结:如何判断 =====\n";
std::cout << "左值: 有名字,能取地址 (&a, &src)\n";
std::cout << "右值: 无名字,不能取地址 (临时对象, 字面量, a+b)\n";
std::cout << "将亡值: std::move(x) 或返回&&的函数调用,属于右值\n";
std::cout << "移动语义: 当对象是右值时,我们可以安全地偷走它的资源。\n";
return 0;
}
三、移动构造函数
移动构造函数的作用:从右值"窃取"资源。
声明形式:
cpp
ClassName(ClassName&& other) noexcept;
参数是非常量右值引用 ClassName&&,因为我们需要修改 other 的内部状态(把它的指针置空等)。通常加上 noexcept,因为移动操作一般不抛异常,这能让标准库容器在重新分配内存时使用移动而不是拷贝(强异常安全保证)。
实现示例:为 Buffer 添加移动构造
cpp
#include <iostream>
#include <algorithm> // std::copy, std::fill
#include <utility> // std::move
class Buffer {
size_t size_;
char* data_;
public:
// 普通构造
Buffer(size_t n) : size_(n), data_(new char[n]) {
std::cout << "普通构造, size = " << n << '\n';
}
// 析构
~Buffer() {
delete[] data_;
std::cout << "析构, size = " << size_ << '\n';
}
// 拷贝构造(左值引用)
Buffer(const Buffer& other) : size_(other.size_), data_(new char[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "拷贝构造, size = " << size_ << '\n';
}
// 移动构造(右值引用)
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
std::cout << "移动构造, size = " << size_ << '\n';
}
// 辅助访问函数(仅用于演示)
size_t size() const { return size_; }
bool isNull() const { return data_ == nullptr; }
// 填充数据(演示用)
void fill(char ch) {
std::fill(data_, data_ + size_, ch);
}
// 打印前几个字符(演示用)
void print(size_t count = 5) const {
if (data_) {
for (size_t i = 0; i < std::min(count, size_); ++i)
std::cout << data_[i];
std::cout << '\n';
}
else {
std::cout << "(null)\n";
}
}
};
// 工厂函数,返回临时对象(触发移动构造或拷贝省略)
Buffer createBuffer(size_t n) {
Buffer temp(n);
temp.fill('X');
return temp; // 返回值优化(RVO)可能会省略移动,但显式 std::move 可强制
}
int main() {
std::cout << "=== 1. 普通构造 ===\n";
Buffer b1(10);
b1.fill('A');
b1.print();
std::cout << "\n=== 2. 拷贝构造 ===\n";
Buffer b2 = b1; // 调用拷贝构造
b2.print();
std::cout << "\n=== 3. 移动构造(将左值转为右值) ===\n";
Buffer b3 = std::move(b1); // 调用移动构造
std::cout << "移动后 b1 的状态: size = " << b1.size()
<< ", data = " << (b1.isNull() ? "nullptr" : "非空") << '\n';
b3.print();
std::cout << "\n=== 4. 移动构造(从函数返回的临时对象) ===\n";
Buffer b4 = createBuffer(8); // 可能触发移动构造(C++17 起保证复制省略)
b4.print();
std::cout << "\n=== 5. 作用域结束,对象依次析构 ===\n";
// 析构顺序:b4, b3, b2, b1(b1 此时为空,delete[] nullptr 安全)
return 0;
}
移动构造做的事情:
1.直接把 other 的资源指针和大小"偷"过来。
2/把 other.data_ 置为 nullptr,size_ 置为 0,这样当 other 析构时 delete\[\] nullptr 是安全的。
四、移动赋值运算符
移动赋值用于将一个右值赋给一个已经存在的对象,同样要"转移资源",并且正确释放当前对象原有的资源。
声明形式:
cpp
ClassName& operator=(ClassName&& other) noexcept;
典型实现步骤:
1.检查是否为自赋值(this != &other),防止把自己移动给自己(虽然左值转右值后通常不会发生,但检查一下无害)。
2.释放当前对象持有的资源。
3.窃取 other 的资源。
4.将 other 置于安全状态。
5.返回 *this。
6.为 Buffer 增加移动赋值。
cpp
#include <iostream>
#include <algorithm> // std::copy, std::fill, std::min
#include <utility> // std::move
class Buffer {
size_t size_;
char* data_;
public:
// ---------- 普通构造 ----------
Buffer(size_t n = 0) : size_(n), data_(n ? new char[n] : nullptr) {
std::cout << "构造, size = " << n << '\n';
}
// ---------- 析构 ----------
~Buffer() {
delete[] data_;
std::cout << "析构, size = " << size_ << '\n';
}
// ---------- 拷贝构造 ----------
Buffer(const Buffer& other) : size_(other.size_), data_(new char[other.size_]) {
std::copy(other.data_, other.data_ + size_, data_);
std::cout << "拷贝构造, size = " << size_ << '\n';
}
// ---------- 移动构造 ----------
Buffer(Buffer&& other) noexcept
: size_(other.size_), data_(other.data_) {
other.size_ = 0;
other.data_ = nullptr;
std::cout << "移动构造, size = " << size_ << '\n';
}
// ---------- 拷贝赋值 ----------
Buffer& operator=(const Buffer& other) {
if (this != &other) {
// 先释放现有资源
delete[] data_;
// 复制新资源
size_ = other.size_;
data_ = new char[size_];
std::copy(other.data_, other.data_ + size_, data_);
}
std::cout << "拷贝赋值, size = " << size_ << '\n';
return *this;
}
// ---------- 移动赋值(用户提供的实现) ----------
Buffer& operator=(Buffer&& other) noexcept {
std::cout << "移动赋值\n";
if (this != &other) {
// 1. 释放现有资源
delete[] data_;
// 2. 窃取数据
size_ = other.size_;
data_ = other.data_;
// 3. 置空源对象
other.size_ = 0;
other.data_ = nullptr;
}
return *this;
}
// ---------- 辅助函数(仅演示用) ----------
size_t size() const { return size_; }
bool isNull() const { return data_ == nullptr; }
void fill(char ch) {
if (data_) std::fill(data_, data_ + size_, ch);
}
void print(size_t count = 5) const {
if (data_) {
for (size_t i = 0; i < std::min(count, size_); ++i)
std::cout << data_[i];
std::cout << '\n';
}
else {
std::cout << "(null)\n";
}
}
};
// ---------- 工厂函数(返回临时对象) ----------
Buffer createBuffer(size_t n) {
Buffer temp(n);
temp.fill('X');
return temp; // 触发移动构造(或复制省略)
}
int main() {
std::cout << "===== 1. 构造并填充 ====\n";
Buffer b1(10);
b1.fill('A');
b1.print();
std::cout << "\n===== 2. 拷贝赋值:b2 = b1 ====\n";
Buffer b2; // 默认构造(size=0, data=nullptr)
b2 = b1; // 拷贝赋值
b2.print();
std::cout << "\n===== 3. 移动赋值:b2 = std::move(b1) ====\n";
b2 = std::move(b1); // 移动赋值,b1 资源转移给 b2
std::cout << "移动后 b1: size = " << b1.size()
<< ", data = " << (b1.isNull() ? "nullptr" : "非空") << '\n';
std::cout << "b2 内容: ";
b2.print();
std::cout << "\n===== 4. 移动赋值:从临时对象 ====\n";
Buffer b3(3); // 普通构造
b3.fill('B');
std::cout << "赋值前 b3: ";
b3.print();
b3 = createBuffer(6); // 临时对象 → 移动赋值
std::cout << "赋值后 b3: ";
b3.print();
std::cout << "\n===== 5. 自赋值测试:a = std::move(a) ====\n";
Buffer b4(4);
b4.fill('C');
b4 = std::move(b4); // 自移动赋值(应安全,不破坏资源)
std::cout << "自赋值后 b4: ";
b4.print();
std::cout << "\n===== 6. 连续移动赋值 ====\n";
Buffer b5(5);
b5.fill('D');
Buffer b6;
b6 = std::move(b5); // b5 → b6
std::cout << "第一次移动后 b5: " << (b5.isNull() ? "null" : "非空") << '\n';
Buffer b7;
b7 = std::move(b6); // b6 → b7
std::cout << "第二次移动后 b6: " << (b6.isNull() ? "null" : "非空") << '\n';
std::cout << "最终 b7: ";
b7.print();
std::cout << "\n===== 7. 作用域结束,析构顺序 ====\n";
return 0;
}
五、std::move 的作用
std::move 并不移动任何东西,它只是一个强制类型转换:将左值无条件转换为对应的右值引用,让它"可移动"。
cpp
template<typename T>
typename remove_reference<T>::type&& move(T&& t) noexcept {
return static_cast<typename remove_reference<T>::type&&>(t);
}
转换后,就可以在语义上表明"我不再需要这个对象的内容",然后交给移动构造或移动赋值。
常见用途:
将左值插入容器时主动移动,避免拷贝:
cpp
std::vector<Buffer> vec;
Buffer buf(500);
vec.push_back(std::move(buf)); // 移动 buf 到 vector,buf 变为空
在类的成员初始化列表中转移所有权:
cpp
class Holder {
Buffer buffer;
public:
Holder(Buffer b) : buffer(std::move(b)) {}
};
注意:被移动后的对象处于"有效但未指定"的状态,通常只能安全地析构或重新赋值。不要假设它还有其他有效内容。
六、编译器自动生成的移动函数(规则简化)
如果你不声明拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数中的任何一个,编译器会为你自动生成移动构造函数和移动赋值运算符(如果有可能),执行成员逐一移动。
但是一旦你手动声明了拷贝构造、拷贝赋值或析构函数中的任何一个,编译器就不会自动生成移动函数(因为这意味着类有特殊的资源管理需求,默认的成员移动可能不安全)。此时若要支持移动,必须手动编写 && 版本。
也可用 = default 显式要求编译器生成:
cpp
class Simple {
std::vector<int> data;
public:
Simple() = default;
Simple(Simple&&) noexcept = default;
Simple& operator=(Simple&&) noexcept = default;
};
七、移动语义在标准库中的体现
C++11 之后,标准库全面支持移动语义:
1.std::vector、std::string 等容器在重新分配内存时,如果元素类型的移动构造声明为 noexcept,就会移动元素而不是拷贝,极大提高性能。
2.std::unique_ptr 只允许移动,禁止拷贝,彻底表达了独占所有权语义。
3.std::thread、std::fstream 等不可拷贝的资源管理类,全部变成了可移动但不可拷贝。
容器插入操作增加了右值引用重载,如 push_back(T&&),可以直接移动临时对象或 4.std::move 后的左值。
八、移动与拷贝省略(Copy Elision)
即使不写移动构造,C++ 编译器也可能通过 RVO/NRVO 将临时对象直接构造到目标位置,省略拷贝/移动。但移动构造的提供可以保证在编译优化无法覆盖的场合(例如按值返回一个函数参数、条件分支返回不同对象等)仍然获得高效行为。
现代 C++ 的最佳实践:需要管理资源的类,同时提供高效的移动构造和移动赋值,并标记为 noexcept。
九、完整示例
下面的例子综合了所有概念:
string版本:
cpp
#include <iostream>
#include <vector>
#include <string>
class MyString {
std::string str;
public:
// 构造函数
MyString(const char* s = "") : str(s) {
std::cout << "构造: " << str << "\n";
}
// 拷贝构造
MyString(const MyString& other) : str(other.str) {
std::cout << "拷贝构造: " << str << "\n";
}
// 移动构造(noexcept,以便 vector 优化)
MyString(MyString&& other) noexcept : str(std::move(other.str)) {
std::cout << "移动构造\n";
}
// 拷贝赋值
MyString& operator=(const MyString& other) {
if (this != &other) {
str = other.str;
std::cout << "拷贝赋值: " << str << "\n";
}
return *this;
}
// 移动赋值
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
str = std::move(other.str);
std::cout << "移动赋值\n";
}
return *this;
}
// 打印
void print() const {
std::cout << (str.empty() ? "(空)" : str) << '\n';
}
};
int main() {
MyString a("hello");
MyString b = std::move(a); // 移动构造
a.print(); // (空)
b.print(); // hello
MyString c("world");
c = std::move(b); // 移动赋值
b.print(); // (空)
c.print(); // hello
// 测试拷贝空对象(移动后的对象)
MyString d(b); // 拷贝构造,b 为空字符串
d.print(); // (空)
// vector 演示移动语义
std::vector<MyString> vec;
vec.push_back(MyString("temp")); // 临时对象移动进容器
vec.push_back(MyString("test")); // 扩容时移动已有元素
}
char版本用Release模式运行:
cpp
#include <iostream>
#include <vector>
#include <cstring>
class MyString {
char* str;
size_t len;
public:
MyString(const char* s = "") : len(std::strlen(s)), str(new char[len + 1]) {
strcpy_s(str, len + 1, s);
std::cout << "构造: " << str << "\n";
}
~MyString() {
delete[] str;
std::cout << "析构\n";
}
// 拷贝构造(防移动后空对象)
MyString(const MyString& other) : len(other.str ? other.len : 0),
str(new char[len + 1]) {
if (other.str) {
strcpy_s(str, len + 1, other.str);
std::cout << "拷贝构造: " << str << "\n";
}
else {
str[0] = '\0';
std::cout << "拷贝构造: (空对象)\n";
}
}
// 移动构造(noexcept,移动后置空)
MyString(MyString&& other) noexcept
: len(other.len), str(other.str) {
other.str = nullptr;
other.len = 0;
std::cout << "移动构造\n";
}
// 拷贝赋值(防移动后空对象)
MyString& operator=(const MyString& other) {
if (this != &other) {
delete[] str;
len = other.str ? other.len : 0;
str = new char[len + 1];
if (other.str) {
strcpy_s(str, len + 1, other.str);
std::cout << "拷贝赋值: " << str << "\n";
}
else {
str[0] = '\0';
std::cout << "拷贝赋值: (空对象)\n";
}
}
return *this;
}
// 移动赋值(noexcept,移动后置空)
MyString& operator=(MyString&& other) noexcept {
if (this != &other) {
delete[] str;
str = other.str;
len = other.len;
other.str = nullptr;
other.len = 0;
}
std::cout << "移动赋值\n";
return *this;
}
void print() const { std::cout << (str ? str : "(空)") << '\n'; }
};
int main() {
MyString a("hello");
MyString b = std::move(a);
a.print(); // (空)
b.print(); // hello
MyString c("world");
c = std::move(b);
b.print(); // (空)
c.print(); // hello
MyString d(b);
d.print(); // (空)
std::vector<MyString> vec;
vec.push_back(MyString("temp"));
vec.push_back(MyString("test"));
return 0;
}