C++右值引用学习笔记

发布于:2025-08-14 ⋅ 阅读:(15) ⋅ 点赞:(0)


前言

右值引用就是右值引用,它和左值引用一样,算是一种引用。不用想其他的东西,回归到语言上,问题的本身即可。
C++11 引入右值引用,并且引入了一整套的规则:值类别,即:左值、纯右值、亡值。
如果缺少值类别的基本知识,请先观看视频。
其实简单来说,就是将各种表达式分类,哪些是左值表达式,哪些是右值(纯右值和亡值都是右值)。右值引用只能被右值表达式初始化。

在右值引用(移动语义)出现之前,我们没有办法简单的区分,你到底是要复制?还是要移动?在右值引用(移动语义)出现后,我们可以了
开发者们约定移动构造这些右值引用的函数,是“转移所有权”
你可以非常粗略的理解为,复制就是复制一份新的,但是移动呢,是把原对象的指向资源的指针,赋给新的对象成员,也就是所谓的转移所有权,通常的实现是转移指向数据的那个指针给新对象就行(当然了,这种移动是取决于你自己的移动构造移动赋值的实现的),自然没有复制开销。

先来一个问题:移动语义有什么作用,原理是什么?
1.作用原理:
利用右值引用(T&&)标识临时对象/可移动对象移动构造函数/赋值运算符直接"窃取"源对象的资源将源对象置于有效但未定义状态(通常为空)
避免了深拷贝带来的性能损耗
2.关键机制:
右值引用绑定到临时对象(右值)
移动操作后源对象必须保持可析构状态
通过std::move将左值转为右值引用(std::move其实不移动任何东西,只是告诉编译器"这个可以移动")
编译器在特定场景自动使用移动语义(如返回值优化)

适用场景
传递大数据时不想复制(比如整个vector)
函数返回临时对象时
管理文件、网络连接等资源的类
排序、洗牌等重新排列元素的操作
写模板代码时保持参数特性

1. 右值是什么

在 C++ 中,右值(Rvalue) 是指那些临时性的、没有持久存储位置、即将被销毁的值。它们代表表达式的"计算结果"或"临时对象",不能出现在赋值语句的左侧(这也是"右值"名称的由来)。右值是 C++ 值类别(value categories)体系的核心概念之一。


1.1 右值的核心特征

  1. 无持久身份(No Persistent Identity)
    没有可长期访问的内存地址(不能使用 & 取地址)

  2. 只能出现在赋值右侧

    int a = 42;   // ✅ 42 是右值(出现在右侧)
    42 = a;       // ❌ 错误!右值不能作为赋值左操作数
    
  3. 短生命周期
    通常是表达式求值过程中的临时对象,在完整表达式结束时被销毁


1.2 右值的具体表现形式

类型 示例 说明
字面量 42, "hello", 3.14, true 除字符串字面量外(它是左值)
算术/逻辑表达式 x + y, a * b, !flag 计算结果为临时值
函数返回值 std::string get_str() 的返回值 返回的非引用类型临时对象
类型转换结果 static_cast<float>(x) 转换生成的临时值
将亡值(xvalue) std::move(obj) 被标记为"可移动"的对象
临时对象 std::string("temp") 显式构造的匿名对象

1.3 右值 vs 左值对比

特性 左值 (Lvalue) 右值 (Rvalue)
持久性 有持久内存地址 临时存在,很快销毁
取地址 &x 合法 &42 非法
赋值能力 可出现在 = 左侧 只能出现在 = 右侧
生命周期 作用域内持续存在 当前表达式结束即销毁
典型例子 变量、函数名、字符串字面量 字面量、临时对象、x+y 的结果

代码示例

#include <iostream>
#include <string>

std::string create_name() {
    return "TemporaryName"; // 返回右值(临时对象)
}

int main() {
    // 左值示例
    int x = 10;          // x 是左值(有内存地址)
    int* ptr = &x;       // ✅ 可获取地址
    
    // 右值示例
    int y = x + 5;       // (x+5) 是右值(临时计算结果)
    // &(x+5);           // ❌ 错误!不能取右值地址
    
    std::string s1 = "Hello"; // "Hello" 是左值(字符串字面量有固定地址)
    std::string s2 = create_name(); // 函数返回右值
    
    // 右值引用绑定
    std::string&& rref = s1 + s2; // ✅ 绑定到临时拼接结果
    // std::string&& bad = s1;    // ❌ 错误!不能直接绑定左值

    // 将左值转为右值
    std::string s3 = std::move(s1); // s1 现在处于"将亡"状态
}

1.4 右值的子类别(C++11 细化)

C++11 将值类别细分为:

  1. 纯右值 (prvalue)
    最"纯粹"的右值:字面量、表达式结果

    42, x + y, create_name()
    
  2. 将亡值 (xvalue)
    "即将被移动"的值,通常由 std::move 生成

    std::move(s1), std::forward<T>(arg)
    
  3. 左值 (lvalue)
    传统意义上的持久对象

📌 广义右值 = prvalue + xvalue


1.5 为什么需要区分右值?

右值的核心价值在于资源高效利用

  1. 实现移动语义
    允许"窃取"右值资源而非深拷贝

    std::vector<int> v1 = {1,2,3};
    std::vector<int> v2 = std::move(v1); // 移动而非拷贝
    // v1 现在为空
    
  2. 实现完美转发
    保持参数原始值类别传递

    template<typename T>
    void relay(T&& arg) {
        target(std::forward<T>(arg));
    }
    
  3. 避免不必要的拷贝
    编译器可直接使用临时对象


1.6 关键总结

  • 右值 = 临时值 + 将亡值,代表资源的"一次性使用权"
  • 右值引用 (T&&) 是操作右值的工具
  • std::move() 将左值标记为右值(不移动数据,仅类型转换)
  • 移动构造函数/赋值运算符是右值引用的主要应用场景

💡 编程启示:识别右值能帮你写出更高效的代码,特别是在涉及资源管理(动态内存、文件句柄等)的场景中。

2. 移动语义与完美转发

2.1 移动语义(Move Semantics)

核心目的:避免不必要的深拷贝,提升性能(特别是处理动态资源时)

实现基础:右值引用(T&&

工作原理

  1. 识别临时对象(右值)
  2. "窃取"资源而非拷贝
  3. 将源对象置于安全状态(可析构但无资源)
class String {
public:
    // 移动构造函数
    String(String&& other) noexcept 
        : data_(other.data_), size_(other.size_) 
    {
        // 窃取资源
        other.data_ = nullptr;  // 置空源对象指针
        other.size_ = 0;        // 置空源对象大小
    }

    // 移动赋值运算符
    String& operator=(String&& other) noexcept {
        if (this != &other) {
            delete[] data_;      // 释放现有资源
            data_ = other.data_; // 窃取资源
            size_ = other.size_;
            other.data_ = nullptr;
            other.size_ = 0;
        }
        return *this;
    }

private:
    char* data_;
    size_t size_;
};

使用场景

String createString() {
    return String("Hello"); // 返回临时对象
}

int main() {
    String s1 = createString(); // 移动构造
    
    String s2;
    s2 = String("World"); // 移动赋值
    
    std::vector<String> vec;
    vec.push_back(String("Move")); // 移动而非拷贝
}

2.2 完美转发(Perfect Forwarding)

核心目的:在泛型代码中保持参数的原始值类别(左值/右值)

关键组件

  1. 通用引用(Universal Reference):T&&
  2. std::forward<T>()

工作原理

template <typename T>
void relay(T&& arg) {
    // 保持 arg 的原始值类别(左值/右值)
    target(std::forward<T>(arg));
}

void target(int& x) { 
    std::cout << "lvalue: " << x << "\n"; 
}

void target(int&& x) { 
    std::cout << "rvalue: " << x << "\n"; 
}

int main() {
    int a = 10;
    relay(a);            // 转发左值 → target(int&)
    relay(20);           // 转发右值 → target(int&&)
    relay(std::move(a)); // 转发将亡值 → target(int&&)
}

输出

lvalue: 10
rvalue: 20
rvalue: 10

2.3 最佳实践指南

  1. 移动语义应用场景

    • 容器操作(vector::push_back
    • 大型对象传递
    • 资源管理类(文件句柄、网络连接)
  2. 完美转发应用场景

    • 工厂函数模板
    • 包装器函数
    • 线程参数传递
  3. std::move 使用原则

    void process(Object&& obj); // 接受右值
    
    Object obj;
    process(std::move(obj)); // 明确放弃所有权
    
    // 此后 obj 处于有效但未定义状态
    obj.reset(); // 必须重置后才能复用
    
  4. 移动后对象状态

    • 必须保持可析构
    • 必须保持可赋值
    • 其他方法调用结果未定义

性能提升关键

  • 避免动态内存分配
  • 避免大数据复制
  • 减少析构开销

💡 黄金法则

  • 对可拷贝类型实现移动操作(3-10倍性能提升)
  • 对只移动类型(unique_ptr)禁用拷贝操作
  • 所有移动操作声明 noexcept
  • 移动后重置源对象状态

2.4 性能对比

class HeavyResource {
    std::vector<int> data; // 1MB 数据
public:
    HeavyResource() : data(1'000'000, 42) {}
    
    // 拷贝构造函数(深拷贝)
    HeavyResource(const HeavyResource&) = default;
    
    // 移动构造函数(指针交换)
    HeavyResource(HeavyResource&&) noexcept = default;
};

// 测试用例
void test() {
    std::vector<HeavyResource> v;
    
    // 拷贝:1ms per element
    HeavyResource h1;
    v.push_back(h1); // 拷贝 → 慢
    
    // 移动:0.01ms per element
    v.push_back(HeavyResource()); // 移动 → 快
}

3. 右值引用与左值引用

在 C++ 中,右值引用(Rvalue Reference)是 C++11 引入的重要特性,它与传统的左值引用(Lvalue Reference)共同构成了引用体系的核心。两者的核心区别在于它们绑定的对象类型不同,直接影响了资源的处理方式。


3.1 核心概念快速对比

特性 左值引用 (T&) 右值引用 (T&&)
绑定对象 左值(有标识、持久化的对象) 右值(临时对象、字面量等)
语法符号 &(如 int& ref = a;) &&(如 int&& ref = 10;)
主要用途 避免拷贝、修改原始对象 实现移动语义、完美转发
生命周期 不延长临时对象生命周期 延长临时对象生命周期
是否可修改 可修改绑定的对象 可修改绑定的对象(通常用于"窃取"资源)

3.2 深入解析

3.2.1 绑定对象的本质区别
  • 左值引用 &
    绑定到左值(Lvalue):有名字、有内存地址、可多次使用的对象。

    int a = 10;         // a 是左值
    int& lref = a;      // 正确:绑定到左值
    int& lref2 = 10;    // 错误!不能绑定到右值
    
  • 右值引用 &&
    绑定到右值(Rvalue):临时对象、字面量、表达式结果等"即将销毁"的值。

    int&& rref1 = 10;                  // 正确:绑定到字面量
    int&& rref2 = std::move(a);        // 正确:绑定到将亡值
    int&& rref3 = some_function();     // 正确:绑定到函数返回的临时对象
    

3.2.2 核心用途差异
  • 左值引用

    • 函数传参避免拷贝(如 void process(const std::string& str))。
    • 别名操作(修改原始对象)。
  • 右值引用

    • 移动语义(Move Semantics):高效转移资源所有权(如动态内存)。
      // 移动构造函数示例
      class Vector {
      public:
          Vector(Vector&& other) noexcept 
              : data_(other.data_), size_(other.size_) {
              other.data_ = nullptr; // 置空源对象,避免资源被释放
          }
      private:
          int* data_;
          size_t size_;
      };
      
    • 完美转发(Perfect Forwarding):保持参数原始类型转发。
      template<typename T>
      void wrapper(T&& arg) {
          // 使用 std::forward 保持 arg 的左值/右值特性
          target_function(std::forward<T>(arg));
      }
      

3.2.3 生命周期管理
  • 左值引用不延长临时对象的生命周期:
    const std::string& s = "hello"; // 合法:常量左值引用可绑定右值
    // 但临时对象 "hello" 的生命周期被延长至 s 的作用域
    
  • 右值引用显式延长临时对象的生命周期:
    std::string&& s = "hello"; // 直接管理临时对象生命周期
    

3.2.4 重要工具函数
  • std::move():将左值强制转换为右值引用(表示"可移动")。
    std::vector<int> v1 = {1, 2, 3};
    std::vector<int> v2 = std::move(v1); // 移动构造,v1 变为空
    
  • std::forward<T>():在泛型代码中保持参数的左值/右值特性。

关键总结

场景 左值引用 右值引用
避免拷贝(只读) ✅ (const T&)
避免拷贝(修改原始对象) ✅ (T&)
高效转移资源(移动语义)
泛型编程(完美转发)
操作临时对象 ❌(常量引用除外)

💡 最佳实践

  • 优先使用 const T& 传递只读参数。
  • 需要修改对象时用 T&
  • 在实现资源管理类(如容器、智能指针)时,务必实现移动构造/赋值函数(使用 T&&)。
  • 在模板中使用 T&&std::forward 实现完美转发。

右值引用是 C++ 高效资源管理的基石,它使得现代 C++ 在性能上实现了质的飞跃(如 std::vector 的插入操作效率提升)。

4. 移动构造函数详解

先看一个简单的例子:

class MyString 
{ 
 // pick implementation from Listing 9.9 
}; 
MyString Copy(MyString& source) // function 
{ 
 MyString copyForReturn(source.GetString()); // create copy 
 return copyForReturn; // return by value invokes copy constructor 
} 
int main() 
{ 
 MyString sayHello ("Hello World of C++"); 
 MyString sayHelloAgain(Copy(sayHello)); // invokes 2x copy constructor 
 return 0; 
}

实例化 sayHelloAgain 时,由于调用了函数 Copy(sayHello),而它按值返回一个
MyString,因此调用了复制构造函数两次。然而,这个返回的值存在时间很短,且在该表达式外不可
用。因此,C++编译器严格地调用复制构造函数反而降低了性能,如果复制的对象很大,对性能的影
响将很严重。

如果 MyString 定义了移动构造函数:

MyString(MyString&& other) noexcept;  // 移动构造

那么返回局部对象时会优先使用移动构造(零开销),省一次复制构造

4.1 移动构造函数的特征与生成规则

核心特征

class MyClass {
public:
    // 移动构造函数
    MyClass(MyClass&& other) noexcept 
        : resource(other.resource) // 1. 转移资源
    {
        // 2. 置空源对象
        other.resource = nullptr; 
        
        // 3. 转移辅助状态
        size = other.size;
        other.size = 0;
    }

    // 必须声明 noexcept!
    // 否则标准库容器将使用拷贝而非移动
};

关键要点

  1. 参数类型MyClass&&(右值引用)
  2. 资源转移:直接接管指针/句柄
  3. 源对象重置
    • 指针置 nullptr
    • 基本类型置 0
    • 保证源对象可安全析构
  4. noexcept 声明
    • 标准库优化关键(vector 重新分配等)
    • 违反可能导致拷贝而非移动

自动生成规则

用户声明 移动构造函数生成
无任何构造/赋值声明 ✅ 自动生成
自定义拷贝构造函数 ❌ 不生成
自定义拷贝赋值运算符 ❌ 不生成
自定义析构函数 ⚠️ 可能不生成

在 C++ 中,类的复制构造函数和移动构造函数的默认生成规则是明确但有所区别的。以下是详细说明:

4.1.1 默认复制构造函数与默认移动构造函数

默认复制构造函数

  • 会自动生成,除非:
    1. 用户显式声明了复制构造函数
    2. 用户声明了移动操作(移动构造/移动赋值)
    3. 用户声明了析构函数(此规则在 C++11 后有所变化)
class DefaultCopy {
    // 自动生成默认复制构造函数
    // 执行成员变量的逐个复制
};

默认移动构造函数

  • 不会自动生成,除非:
    1. 用户没有声明任何复制操作(复制构造/复制赋值)
    2. 用户没有声明任何移动操作
    3. 用户没有声明析构函数
class AutoMove {
    // 满足条件时自动生成默认移动构造函数
    // 执行成员变量的逐个移动
};

4.1.2 生成规则总结(C++11 起)与默认实现行为

用户声明的函数 默认复制构造 默认移动构造
无声明 ✅ 生成 ✅ 生成
声明了析构函数 ✅ 生成 不生成
声明了复制操作(构造/赋值) ❌ 不生成 ❌ 不生成
声明了移动操作(构造/赋值) ✅ 生成 ❌ 不生成
声明了复制操作+析构函数 ❌ 不生成 ❌ 不生成

注:C++11 前没有移动语义,规则更简单

默认实现的行为

构造函数 行为
复制构造 对每个成员调用复制构造(内置类型直接复制,类类型调用其复制构造)
移动构造 对每个成员调用移动构造(内置类型直接复制,类类型调用其移动构造)

代码验证示例

#include <iostream>

class Test {
public:
    Test() = default;
    
    // 用户声明析构函数会阻止默认移动构造生成
    ~Test() {}
};

int main() {
    std::cout << std::boolalpha;
    
    // 验证默认函数存在性
    std::cout << "Default copy: " 
              << std::is_copy_constructible_v<Test> << "\n";  // true
    
    std::cout << "Default move: " 
              << std::is_move_constructible_v<Test> << "\n";  // false
}

4.1.3 最佳实践建议

  1. 遵循三五法则

    • 如果声明了析构函数/复制操作,应该声明所有特殊成员函数
    class RuleOfFive {
    public:
        ~RuleOfFive();                      // 1. 析构
        RuleOfFive(const RuleOfFive&);       // 2. 复制构造
        RuleOfFive& operator=(const RuleOfFive&); // 3. 复制赋值
        RuleOfFive(RuleOfFive&&) noexcept;   // 4. 移动构造
        RuleOfFive& operator=(RuleOfFive&&) noexcept; // 5. 移动赋值
    };
    
  2. 显式控制默认行为

    class ExplicitControl {
    public:
        // 显式要求默认实现
        ExplicitControl(ExplicitControl&&) = default;
    };
    
  3. 禁用不需要的操作

    class NonCopyable {
    public:
        NonCopyable(const NonCopyable&) = delete;
        NonCopyable& operator=(const NonCopyable&) = delete;
    };
    

4.1.4 特殊场景处理与例外情况

class Base {
public:
    virtual ~Base() = default;  // 声明虚析构
    
    // 由于声明了析构函数,需要显式要求移动操作
    Base(Base&&) = default;
    Base& operator=(Base&&) = default;
};

class Derived : public Base {
    // 自动生成复制/移动构造
    // 会调用基类的对应操作
};

例外情况

class NoMove {
    std::unique_ptr<int> ptr;  // 移动构造被删除的成员
};

int main() {
    static_assert(!std::is_move_constructible_v<NoMove>, 
                  "Won't compile: unique_ptr blocks move");
}

4.2 C++ 移动构造函数调用时机

移动构造函数是 C++11 引入的核心特性,用于高效转移资源所有权而非复制。它在以下场景中会被调用:

4.2.1 详细场景分析

适用场景
传递大数据时不想复制(比如整个vector)
函数返回临时对象时
管理文件、网络连接等资源的类
排序、洗牌等重新排列元素的操作
写模板代码时保持参数特性

场景 1:显式移动语义

MyClass a;
MyClass b = std::move(a); // 移动构造

场景 2:函数参数传递

void process(MyClass&& obj); // 接受右值引用

MyClass obj;
process(std::move(obj));    // 不调用移动构造,直接绑定

场景 3:工厂函数返回

MyClass create() {
    MyClass local;
    return local; // 可能调用移动构造(若未RVO优化)
}

场景 4:容器操作

std::vector<MyClass> vec;

// emplace_back直接构造(最优)
vec.emplace_back(); 

// push_back临时对象
vec.push_back(MyClass()); // 移动构造

// 插入左值对象(复制构造)
MyClass obj;
vec.push_back(obj); 

// 插入右值对象(移动构造)
vec.push_back(std::move(obj)); 

场景 5:异常安全保证

try {
    MyClass resource;
    throw std::runtime_error("Error");
} 
// 栈解退时调用移动构造(若可用)更高效
catch (...) {} 

4.2.2 移动构造调用优先级

编译器选择构造函数的决策流程:

初始化表达式
是右值?
尝试移动构造
尝试复制构造
移动构造可用?
调用移动构造
复制构造可用?
调用复制构造
编译错误

关键总结

  • 默认复制构造函数会自动生成,除非有显式声明或移动操作声明
  • 默认移动构造函数不会自动生成,除非满足特定条件(无任何特殊成员声明)
  • 声明析构函数会阻止默认移动构造生成
  • 内置类型成员在移动构造中执行复制而非移动
  • 现代 C++ 应显式声明需要的行为(=default/=delete

个人感觉移动构造、复制构造这些都是“君子协定”。实际上c++是允许移动构造函数里搞复制,复制构造函数里搞移动(别这样做!)。

右值引用与左值引用最大的不同就是名字不同,这样就可以触发不同的的重载函数了。std::move某种程度上更像是注释,快不快完全取决于移动构造函数是如何写的。
c++里这个最诡异的是move他没有任何功能。我觉得叫它moveable才是最准确的。
把右值当成参数 然后交给实际的function去做移动操作。
std::move只是无条件转换为右值引用
实际移动操作由类的移动构造函数/赋值运算符完成
设计上分离了"移动许可"和"移动实现!

面试官可能追问
1.std::move做了什么?为什么它自己不移动任何东西?std::move只是无条件转换为右值引用实际移动操作由类的移动构造函数/赋值运算符完成设计上分离了"移动许可"和"移动实现"
2.移动后源对象的状态要求?
必须处于有效但未指定状态
必须允许析构和赋值
典型实现是将源对象的指针置nullptr
3.什么时候编译器会自动使用移动语义?函数返回局部对象时(RVO/NRVO)
抛出异常时,临时对象初始化新对象时
容器重新分配内存时(如vector扩容)
4.如何判断一个类是否该实现移动语义?
类管理外部资源(内存、文件句柄等)
移动比拷贝能显著提高性能
拷贝代价高或不可用(如unique_ptr)


网站公告

今日签到

点亮在社区的每一天
去签到