需要预先学习:


1. 移动语义

移动语义(Move Semantics)是 C++11 引入的一项重要特性,它的核心思想是:当你把一个对象的资源"转移"到另一个对象时,不需要做深拷贝,而是直接把指针/句柄"偷"过来,让原对象变成一个空壳。这样可以避免大量不必要的拷贝开销,大幅提升性能。


1.1 拷贝的缺点

在传统 C++ 中,对象的传递依赖拷贝。对于管理堆内存的类(如 std::string、std::vector),拷贝意味着:

  1. 分配新的堆内存
  2. 逐字节复制原对象的数据
  3. 原对象和目标对象各自拥有独立的内存
#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& 的关键特征:

  1. 可以修改 — 右值引用指向的对象即将被销毁,允许安全地修改它的内容,避免不必要的拷贝
  2. 语义明确 — 函数重载时,编译器能区分左值和右值,从而选择不同的实现
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

移动的核心操作只有三步:

  1. 接管指针 — 直接把对方的 _data 拷过来(浅拷贝)
  2. 接管元数据 — 同步 _size 等信息
  3. 置空原对象 — 把对方的 _data 设为 nullptr,防止析构时释放已被转移的内存

注意:被移动后的对象处于"有效但未指定"状态——你可以安全地析构它,或给它赋新值,但不应该再访问它原有的内容。


2. 右值的划分与 std::move

在深入 std::move 之前,需要先理清 C++ 中右值的分类体系。


2.1 纯右值与将亡值

C++11 将表达式按"身份"(identity)和"可移动性"(movability)分为以下类别:

[左值、纯右值与将亡值]

类别 有身份(可寻址) 可移动 典型例子
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;
}

典型使用场景:

  1. 向容器中放入大对象:

    std::vector<std::string> vec;
    std::string big = "a very long string...";
    vec.push_back(std::move(big));   // 移动而非拷贝
  2. 从函数返回局部对象时不需要 std::move — 编译器会自动使用移动:

    std::string createString()
    {
        std::string result = "hello";
        return result;               // 自动移动(NRVO/移动构造)
        // 不要写 return std::move(result); — 会抑制 NRVO 优化
    }
  3. 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 的");