C++——8.移动语义初探(移动构造、移动赋值)

一、为什么要引入移动语义

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;
}
相关推荐
2601_962341301 小时前
计算机毕业设计之jsp考研在线复习平台
java·大数据·开发语言·hadoop·python·考研·课程设计
凯瑟琳.奥古斯特1 小时前
力扣1008:前序重建BST
开发语言·c++·算法·leetcode·职场和发展
Alvin's Tech Blog1 小时前
内存泄漏会导致操作系统永久丢失内存吗?它会导致应用程序崩溃吗?
c++·内存管理·内存泄漏
会周易的程序员1 小时前
使用 LuaBridge 封装 C++ 日志库 microLog 为 Lua 模块
c++·物联网·junit·lua·日志·iot·aiot
2zcode1 小时前
项目文档:基于MATLAB图像处理的饮料瓶灌装液位检测系统设计与实现
开发语言·图像处理·matlab
月疯2 小时前
np.where()[0]的用法
开发语言·python·numpy
wbs_scy2 小时前
仿 muduo 高并发服务器项目:从 timerfd 到时间轮实现定时任务机制
linux·服务器·c++
z落落2 小时前
C#五大加密算法(AES、DES、MD5、RSA、SHA)
开发语言·c#
namexingyun2 小时前
Scaling Law bug实战启示:从“虚胖“到“精瘦“的算力效率革命
开发语言·网络·人工智能·bug·ai编程