C++11 右值引用与移动语义

一、左值 vs 右值

  1. 左值

    • 左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。
    int a = 10;    // a 是左值
    int& b = a;    // b 是左值引用
  2. 右值

    • 右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。(我们可以将右值分为两种,第一种是纯右值,比如常量之类的,第二种则是将亡值,特点是既有身份(可以取地址),又允许被移动,被移动是指其资源可以被安全地转移,而无需执行深拷贝,比如自定义类型
    42;            // 字面量是右值
    a + b;         // 表达式结果是右值
    int func() { return 1; } 
    func();        // 函数返回的临时值是右值

二、右值引用

  1. 语法
    右值引用用 && 表示,​只能绑定到右值

    int&& rref = 42;        // 正确:右值引用绑定到字面量
    int&& rref2 = a + b;    // 正确:绑定到表达式结果
  2. 核心目的

    • 实现移动语义:允许“窃取”右值的资源,避免不必要的深拷贝。
    • 完美转发(Perfect Forwarding)​:保持参数类型不变地传递(此处主要讨论移动语义)。
        需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可 以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。

三、左值引用与右值引用比较

左值引用总结:
1. 左值引用只能引用左值,不能引用右值。
2. 但是const左值引用既可引用左值,也可引用右值。
int main()
{
    // 左值引用只能引用左值,不能引用右值。
    int a = 10;
    int& ra1 = a;   // ra为a的别名
    //int& ra2 = 10;   // 编译失败,因为10是右值
    // const左值引用既可引用左值,也可引用右值。
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}
右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
int main()
{
 // 右值引用只能右值,不能引用左值。
 int&& r1 = 10;
 
 // error C2440: “初始化”: 无法从“int”转换为“int &&”
 // message : 无法将左值绑定到右值引用
 int a = 10;
 int&& r2 = a;
 // 右值引用可以引用move以后的左值
 int&& r3 = std::move(a);
 return 0;
}

四、左值引用的短板

在C++中,左值引用(T&)和右值引用(T&&)各自有不同的设计目标和适用场景。左值引用虽然功能强大,但在某些场景下存在明显短板,而右值引用的引入正是为了解决这些问题。

1. ​无法高效管理临时对象
  • 问题:左值引用只能绑定到左值(有持久状态的对象),若要将临时对象(右值)传递给函数,必须使用 const T&(常量左值引用),但这会带来两个问题:
    1. 无法修改对象const 引用禁止对临时对象进行修改。
    2. 强制拷贝:如果函数需要持有对象(而非仅读取),则必须进行深拷贝,导致性能损耗。
  • 示例
    void process(const std::string& str) {
        // 若需要存储 str,必须拷贝:
        std::string local_str = str; // 深拷贝,性能低
    }
    
    process("Hello"); // 传递临时对象(右值)
2. ​资源无法转移所有权
  • 问题:左值引用只能访问对象,但无法安全地“窃取”其资源(如动态内存)。当需要将资源从一个对象转移到另一个对象时,必须进行深拷贝,效率低下。
  • 示例
    class BigData {
        int* data; // 大量动态内存
    public:
        // 拷贝构造函数(深拷贝)
        BigData(const BigData& other) {
            data = new int[1000000];
            std::copy(other.data, other.data + 1000000, data);
        }
    };
    
    BigData a;
    BigData b = a; // 深拷贝,耗时且内存占用翻倍
3. ​无法支持完美转发
  • 问题:在泛型编程中,模板函数需要将参数原封不动(保留左值/右值属性)传递给其他函数。左值引用无法区分传入的是左值还是右值,导致转发时可能丢失值类别信息。
  • 示例
    template<typename T>
    void wrapper(T& arg) {
        func(arg); // 参数永远是左值,无法转发右值
    }
    
    wrapper(42); // 错误:左值引用无法绑定右值

五、右值引用如何解决这些问题

1. ​移动语义​
  • 核心思想:通过右值引用直接“窃取”临时对象或即将销毁对象的资源,避免深拷贝。
  • 实现方式
    • 定义移动构造函数和移动赋值运算符,接受右值引用参数。
    • 转移资源所有权,将源对象置于可安全析构的状态(如置空指针)。
  • 示例
    class BigData {
        int* data;
    public:
        // 移动构造函数
        BigData(BigData&& other) noexcept {
            data = other.data;    // 直接接管资源
            other.data = nullptr; // 置空源对象指针
        }
    };
    
    BigData a;
    BigData b = std::move(a); // 调用移动构造函数,无深拷贝

对于管理资源的类,传统的拷贝构造函数会进行深拷贝:

class String {
public:
    char* data;
    
    // 拷贝构造函数(深拷贝)
    String(const String& other) {
        data = new char[strlen(other.data) + 1];
        strcpy(data, other.data);
    }
};

当拷贝临时对象(右值)时,深拷贝是低效的,因为临时对象很快会被销毁。

移动构造函数通过“窃取”右值的资源,避免深拷贝:

class String {
public:
    char* data;

    // 移动构造函数
    String(String&& other) noexcept {
        data = other.data;    // 直接接管资源
        other.data = nullptr; // 置空原对象,避免重复释放
    }
};
class BigData {
    int* buffer; // 大量动态内存
public:
    // 移动构造函数(接受将亡值)
    BigData(BigData&& other) noexcept 
        : buffer(other.buffer) {  // 直接接管资源
        other.buffer = nullptr;   // 置空原对象的资源
    }
};

BigData a;
BigData b = std::move(a); // a 被转换为将亡值,触发移动构造函数
#include <iostream>
#include <vector>

class HeavyData {
public:
    // 默认构造函数(分配资源)
    HeavyData() {
        data = new int[1000000]; // 模拟大型资源
        std::cout << "Default Constructor\n";
    }

    // 移动构造函数
    HeavyData(HeavyData&& other) noexcept {
        data = other.data;      // 窃取资源
        other.data = nullptr;   // 置空原对象资源
        std::cout << "Move Constructor\n";
    }

    // 析构函数
    ~HeavyData() {
        delete[] data;
        std::cout << "Destructor\n";
    }

private:
    int* data = nullptr;
};

// 函数返回一个局部 HeavyData 对象(传值返回)
HeavyData createHeavyData() {
    HeavyData localObj;  // 局部对象
    return localObj;     // 返回值时触发移动构造(而非拷贝)!
}

int main() {
    HeavyData obj = createHeavyData(); 
    return 0;
}
  • 关键点
    • 参数为右值引用 String&&
    • 直接“窃取”资源,不分配新内存。
    • 将原对象的资源指针置空,确保析构安全。

当使用右值初始化对象时,优先调用移动构造函数:

String s1 = "Hello";       // 调用普通构造函数
String s2 = s1;            // 调用拷贝构造函数(s1 是左值)
String s3 = std::move(s1); // 调用移动构造函数(std::move 将左值转为右值)
String s4 = String("World"); // 调用移动构造函数(临时对象是右值)
2、移动赋值运算符

类似地,移动赋值运算符用于“窃取”右值的资源:

class String {
public:
    // 移动赋值运算符
    String& operator=(String&& other) noexcept {
        if (this != &other) {
            delete[] data;       // 释放当前资源
            data = other.data;   // 接管新资源
            other.data = nullptr;
        }
        return *this;
    }
};
  1. noexcept 声明
    移动构造函数/赋值运算符应标记为 noexcept,以便标准库容器安全使用移动语义:

    String(String&& other) noexcept { ... }

六、std::move 的作用

  • 功能:将左值强制转换为右值引用,表示该对象可以被“移动”。
  • 示例
    String s1 = "Hello";
    String s2 = std::move(s1); // s1 的资源被转移到 s2,s1 变为空
  • 注意std::move 本身不进行任何移动操作,只是类型转换。

七、移动语义的优势

  1. 性能提升
    避免不必要的深拷贝,尤其适用于:

    • 容器操作(如 std::vector 的 push_back)。
    • 大对象(如文件、网络连接)。
  2. 资源所有权转移
    在传递临时对象时,直接转移资源而非复制。

八、万能引用

1. ​定义

万能引用又被成为引用折叠,如果传左值就是左值引用,传右值就是右值引用,但其成立需满足两个条件:

  • 类型推导T 必须是通过模板参数推导得到的类型。
  • 引用折叠:通过引用折叠规则处理推导结果。
2. ​语法示例
template<typename T>
void func(T&& arg) { 
    // arg 是万能引用,可以绑定到左值或右值
}
3. ​类型推导规则
  • 若传入 ​左值
    int x = 42;
    func(x); // T 推导为 int&,arg 类型折叠为 int&
  • 若传入 ​右值
    func(42); // T 推导为 int,arg 类型为 int&&

九、完美转发

1. ​核心目标

将函数模板的参数 ​以原始值类别(左值/右值)​ 传递给其他函数,避免不必要的拷贝或类型丢失。

2. ​实现工具
  • std::forward<T>:根据 T 的类型,将参数转换为左值或右值引用。
    template<typename T>
    void wrapper(T&& arg) {
        // 将 arg 按原始类型传递给其他函数
        target(std::forward<T>(arg));
    }
3. ​工作原理
  • 若 arg 绑定到 ​左值
    • T 推导为 T&std::forward<T>(arg) 返回 T&(左值)。
  • 若 arg 绑定到 ​右值
    • T 推导为 Tstd::forward<T>(arg) 返回 T&&(右值)。
4. ​对比 std::move
  • std::move 无条件将参数转为右值。
  • std::forward 有条件地保持值类别。

十、注意事项

  1. 避免过度转发
    仅在需要保留值类别时使用 std::forward,否则直接传递参数即可。

  2. 万能引用与重载冲突
    万能引用模板可能意外匹配非预期类型,需约束模板参数:

    template<typename T>
    auto func(T&&) -> std::enable_if_t<std::is_integral_v<T>> { ... }
  3. 引用折叠的边界情况
    明确理解 T& && 折叠为 T&T&& && 折叠为 T&&

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值