C++98/03 只有左值引用 &,拷贝对象时会产生大量不必要深拷贝,性能损耗严重。C++11 引入右值引用 &&,诞生移动语义、移动构造、移动赋值,通过转移资源所有权替代拷贝,大幅减少内存拷贝开销。

1. 左值与右值

1.1 左值(lvalue,locatevalue)

  • 可以取地址,能够修改的持久存在对象,生命周期长
int a = 10;
a = 20;    // a 左值,可赋值、可取 &a
int arr[5];
arr[0] = 1; // arr[0] 左值
string s = "abc";
s[0] = 'x'; // s[0] 左值

1.2 右值(rvalue,readvalue)

  • 纯右值(prvalue):字面量、运算表达式、函数返回临时对象
100; 3.14; "hello"; a + b; string("temp");
  • 将亡值 xvalue:通过 std::move 转换后的对象,即将销毁、可移动资源

2. 左值引用 & 与右值引用 && 核心特性

2.1 左值引用 T&

  • 只能绑定左值,不能绑定临时右值:
int a = 10;
int& r1 = a;    //  绑定左值
int& r2 = 20;   // 报错,不能绑定右值字面量
// const 左值引用特殊兼容:可绑定左值/右值
const int& r3 = 20; // 允许,临时对象生命周期延长

2.2 右值引用 T&&(C++11 新增)

  • 只能绑定右值(纯右值 / 将亡值),专门捕获临时对象:
int&& rr1 = 100;        // 绑定纯右值
int a = 10;
int&& rr2 = a;          //  报错,a是左值,不能绑定普通左值
int&& rr3 = std::move(a);//  move将左值转为将亡值,可绑定

2.3 && 特性:模板折叠引用

  • 函数模板中 T&& 不是单纯右值引用,会根据实参类型自动折叠:
  • 传入左值 T& → 折叠为 T& && = T&(左值引用)
  • 传入右值 T → 折叠为 T&& && = T&&(右值引用)
template<typename T>
void func(T&& val) // 万能引用,既能接收左值也能接收右值
{
}
int main()
{
    int x = 10;
    func(x);    // x左值 → T=int&,参数类型 int&
    func(20);   // 20右值 → T=int,参数类型 int&&
}
  • 只有模板参数 T&& 才是万能引用;固定类型 string&& 只是单纯右值引用,无折叠特性。

2.4 右值引用延长临时对象生命周期

  • 普通临时对象语句结束立刻销毁;绑定给 T&& 后,临时生命周期和引用变量同步:
string&& s = string("临时字符串");
// 临时对象不会立刻销毁,直到s离开作用域

3. 为什么需要右值引用 &&?

3.1 传统拷贝构造产生昂贵深拷贝,性能差

  • 自定义类持有堆内存、容器资源时,拷贝构造会完整复制一份堆数据,临时对象拷贝后立刻销毁,拷贝完全多余。
    示例:无移动语义的低效拷贝
class Str
{
public:
    char* buf;
    Str(const char* str)
    {
        buf = new char[strlen(str)+1];
        strcpy(buf, str);
        cout << "构造:开辟堆内存" << endl;
    }
    // 拷贝构造:深拷贝,重新分配内存
    Str(const Str& other)
    {
        buf = new char[strlen(other.buf)+1];
        strcpy(buf, other.buf);
        cout << "拷贝构造:重新分配堆内存(开销大)" << endl;
    }
    ~Str() { delete[] buf; cout << "析构:释放堆内存" << endl; }
};

Str createTemp()
{
    return Str("测试临时对象"); // 返回临时右值
}

int main()
{
    Str s = createTemp();
    // C++98流程:1.创建临时Str 2.拷贝构造s 3.销毁临时对象
    // 两次内存分配、两次释放,拷贝完全多余!
    return 0;
}

3.2 无法复用即将销毁的临时对象资源

  • 临时对象马上要析构释放堆内存,但拷贝构造依然重复开辟内存,硬件资源白白浪费。
  • 右值引用的解决方案
    捕获临时右值,新增移动构造函数:不拷贝资源,直接转移堆内存指针所有权,零拷贝开销。

4. 移动构造函数 & 移动赋值函数

4.1 移动构造函数语法

  • 参数为本类右值引用 类名&&,作用:窃取传入临时对象的资源,置空原对象避免析构重复释放。
类名(类名&& 源对象) noexcept   //noexcept :告诉编译器该函数无异常,标准库容器优化内存分配。
{
    // 1. 直接转移堆资源指针
    // 2. 将源对象指针置空,防止源对象析构释放内存
}

#include <iostream>
#include <cstring>
using namespace std;

class Str
{
public:
    char* buf;
    // 普通构造
    Str(const char* str)
    {
        buf = new char[strlen(str)+1];
        strcpy(buf, str);
        cout << "构造:开辟堆内存\n";
    }

    // 拷贝构造:深拷贝
    Str(const Str& other)
    {
        buf = new char[strlen(other.buf)+1];
        strcpy(buf, other.buf);
        cout << "拷贝构造:深拷贝\n";
    }

    // 移动构造:右值引用参数,转移资源
    Str(Str&& other) noexcept
    {
        // 直接抢夺堆内存指针,不重新分配
        buf = other.buf;
        // 源对象置空,析构时不会释放内存
        other.buf = nullptr;
        cout << "移动构造:转移资源,无内存拷贝\n";
    }

    // 移动赋值运算符
    Str& operator=(Str&& other) noexcept
    {
        if(this == &other) return *this; // 防止自赋值
        // 释放当前对象旧资源
        delete[] buf;
        // 转移资源
        buf = other.buf;
        other.buf = nullptr;
        cout << "移动赋值:转移资源\n";
        return *this;
    }

    ~Str()
    {
        if(buf != nullptr)
        {
            delete[] buf;
            cout << "析构:释放堆内存\n";
        }
        else
        {
            cout << "析构:空对象,无需释放\n";
        }
    }
};

Str createTemp()
{
    return Str("临时字符串");
}

int main()
{
    Str s = createTemp();
    // 触发移动构造,仅一次内存开辟,无多余拷贝

    Str s2("第二个字符串");
    s2 = createTemp(); // 触发移动赋值
    return 0;
}

4.2 移动语义触发规则

  • 传入纯右值(临时对象):优先调用移动构造 / 移动赋值
  • 传入普通左值:只能调用拷贝构造,不会触发移动
  • 手动使用 std::move(左值):将左值强制转为将亡值,主动触发移动
Str a("原始对象");
Str b = a;          // 左值,调用拷贝构造
Str c = move(a);    // move转换为将亡值,调用移动构造,a资源被掏空

4.3 移动构造 vs 拷贝构造对比表

特性 拷贝构造 const T& 移动构造 T&& noexcept
参数绑定 左值、const 右值 仅右值 /std::move转换的将亡值
资源处理 深拷贝,重新分配堆内存 转移指针所有权,无内存分配
性能开销 高,大量内存拷贝 极低,仅拷贝指针
原对象状态 资源完整保留 资源被掏空,变为空有效对象
适用场景 需要保留原对象数据 临时对象、不再使用的左值

5. std::move 原理

  • std::move 本质只是强制类型转换:把任意对象转为对应类型的右值引用,不移动任何资源,只是给编译器信号 “这个对象可以被移动”。
template<typename T>
decltype(auto) move(T&& val)
{
    return static_cast<remove_reference_t<T>&&>(val);
}

6. std::forward

  • 万能引用参数如果直接传递会变成左值,std::forward 保留实参左右值属性:
template<typename T>
void wrapper(T&& arg)
{
    target(std::forward<T>(arg)); // 完美转发,右值仍为右值,触发移动
}