蔚淦丞
发布于 2026-09-11 / 10 阅读
0
0

左值、右值、左值引用、右值引用、move语义

一、定义

  • 左值(Lvalue):有名字、可被取地址、生命周期稳定的值

  • 右值(Rvalue):没有名字、临时产生、用完即销毁的值

所有C++表达式结果,只有两种身份:左值 或 右值。

二、左值 & 右值 详细区分

1. 左值 Lvalue

特点:

  • 有变量名

  • 可以取地址

  • 可以出现在赋值号左边

  • 内存持久,不会马上销毁

例子:

int a = 10;   // a 是左值
int b = a;    // a 是左值
&a;           // 能取地址 = 左值实锤

2. 右值 Rvalue

特点:

  • 没有名字、临时值

  • 不能取地址

  • 不能放在赋值号左边

  • 表达式结束立刻销毁

两类典型右值:

  1. 纯数值:10、3.14、'a'

  2. 表达式临时结果:x+1、func()返回的临时对象

例子:

int a = 10 + 5; // 10+5 是右值
10 = a;          // 报错!右值不能被赋值
&(10+5);        // 报错!右值不能取地址

三、左值引用 & 右值引用(核心重点)

1. 左值引用 & (C++98 传统引用)

语法:类型& 变量

只能绑定左值,不能绑定普通右值。

int a = 10;
int& ra = a;   // ✅ 合法:左值引用绑定左值
int& rb = 10;  // ❌ 报错:左值引用不能绑右值

const 左值引用(万能引用)

const 左值引用可以绑定左值、也可以绑定右值

const int& r = 100; // ✅ 合法

这是函数参数大量用 const T& 的根本原因:兼容所有实参

关键大坑:const左值引用虽然能接收右值,但只读不可修改,无法实现资源窃取,因此绝对不能作为移动构造函数的形参

2. 右值引用 && (C++11 新增)

语法:类型&& 变量

专门绑定右值、临时对象不能绑定普通左值

int&& r1 = 100;   // ✅ 绑定右值
int a = 10;
int&& r2 = a;      // ❌ 报错:不能绑左值

重要:右值引用变量本身是左值

看似矛盾,但:

  • 100 是右值

  • int&& r = 100

  • r 这个变量,是左值!!!(有名字、可取地址)

这是移动语义、完美转发的底层根源:所有具名变量,一律是左值

四、移动构造函数核心

1. 为什么移动构造必须用 T&&,不能用 const T&?

核心答案:

  • const T& 确实能接收右值,但它是只读常量引用

  • 移动构造的本质:窃取临时对象的堆资源、置空原对象

  • const 修饰的对象无法修改,不能窃取资源、不能置空原对象,完全做不了移动语义

结论:移动构造函数形参必须是 非const右值引用 T&&,唯一选择。

2. 拷贝构造 vs 移动构造 终极区别

// 拷贝构造:左右值都能接,只读,只能深拷贝
String(const String& other);

// 移动构造:只接右值,可修改,直接窃取资源,零拷贝
String(String&& other);

五、std::move 原理与作用

move 不是移动,只是强制类型转换!

作用:无条件把任意左值,强制转换成右值

本质:static_cast<T&&>(val)

适用场景:明确不需要原对象时,主动触发移动构造,提升效率。

六、完美转发 std::forward 完整版

1. 核心定义

完美转发:模板函数中,保留实参原本的左值/右值属性,原样转发给下层函数,不丢失类型特性。

2. 前置知识:万能引用 + 引用折叠

模板中的 T&& 不是普通右值引用,是万能引用,可适配左右值:

引用折叠规则:

  • T& + & = T&

  • T& + && = T&

  • T&& + & = T&

  • T&& + && = T&&

3. 关键问题:forward 能不能给非模板函数用?

语法上可以,业务上没必要

  • forward 本身是模板函数,任意场景都能调用,不限制目标函数是否为模板

  • 普通函数参数类型固定(如 void func(Test&& x)),左右值属性确定

  • 此时用 forward 和直接 move 效果一致,纯属多余

forward 的唯一价值场景:模板万能引用 T&&

只有模板场景,实参左右值不确定,才需要 forward 动态保留属性,move 无法替代。

4. 为什么完美转发必须用 forward?(经典坑点)

模板接收右值后,参数变成具名变量(左值),丢失右值属性:

template<typename T>
void wrapper(T&& x)
{
    another(x); // x是具名左值!哪怕原本传的是右值,只会触发拷贝
    another(std::forward<T>(x)); // 还原原始属性,右值进右值、左值进左值
}

5. move 和 forward 终极对比

  • std::move:无条件转右值,不管原本是什么类型,强制移动

  • std::forward:有条件转换,原汁原味保留左右值属性

七、最全代码对比(一眼看懂区别)

int main()
{
    int a = 10;  

    // 1. 普通左值引用
    int& r1 = a;      // ✅
    // int& r2 = 10;  // ❌

    // 2. const左值引用
    const int& r3 = a;  // ✅
    const int& r4 = 10; // ✅

    // 3. 右值引用
    int&& r5 = 10;     // ✅
    // int&& r6 = a;   // ❌

    // 4. move强制转右值
    int&& r7 = std::move(a); // ✅
}

八、总结

  1. 左值:有名、可取地址、持久。变量都是左值。

  2. 右值:无名临时量、不可取地址、语句结束销毁。

  3. 左值引用&:只能绑左值,不能绑临时右值。

  4. const左值引用:万能引用,左右值都能绑,但只读,无法实现移动语义。

  5. 右值引用&&:只绑临时右值,可修改资源,是移动构造唯一合法形参。

  6. 具名右值引用变量是左值,传参会丢失右值属性,是所有问题的根源。

  7. std::move:仅将左值强转为右值,无实际移动,无条件转换。

  8. std::forward:仅模板万能引用场景生效,完美保留实参左右值属性,实现完美转发。

  9. forward非模板可用但无用,只有搭配T&&万能引用才有不可替代的价值。


评论