当前位置: 首页 > news >正文

C++ 左值、右值、左值引用、右值引用

1、左值与右值

左值和右值是表达式的属性,核心区别在于:能否取地址、是否有持久的存储

1.1 左值:有名字、能取地址、可被修改(通常)

左值是 “可以放在赋值号左边” 的表达式(但并非绝对,如 const 左值不能被修改),它有明确的内存地址,生命周期较长(如变量)。

int a = 10; // a 是左值(有名字、能取地址)
int* p = &a; // 合法:左值可以取地址const int b = 20; // b 是 const 左值(有名字、能取地址,但不能修改)
// b = 30; // 错误:const 左值不可修改int arr[5]; 
arr[0] = 1; // arr[0] 是左值(数组元素有地址)

1.2 右值:无名字、不能取地址、临时存在

右值是 “只能放在赋值号右边” 的表达式,通常是临时结果(如字面量、表达式计算结果),没有持久的内存地址,生命周期短暂(表达式结束后销毁)。

int a = 10;
int b = 20;// 以下都是右值
100; // 字面量(无名字,不能取地址)
a + b; // 表达式结果(临时值,无名字)
func(); // 函数返回值(非引用类型时,是临时值)

关键特征

  • 右值不能被取地址:&(a + b) 会编译报错(无法对临时值取地址)。
  • 右值是 “消耗品”:使用后就会销毁(除非被保存到左值中)。

右值的细分

C++11 后,右值又分为两种,但日常使用中无需严格区分,知道它们都是右值即可:

  • 纯右值(Prvalue):字面量(如 10)、表达式结果(如 a + b)、非引用返回的函数结果。
  • 将亡值(Xvalue):通过 std::move 转换后的左值(本质是 “即将被销毁的左值”,可被移动)。

2、左值引用:绑定左值的 “别名”

左值引用是最常用的引用类型,用 & 表示,只能绑定左值,本质是给左值起一个 “别名”,操作引用等价于操作原对象。

2.1 基本用法

int a = 10;
int& ref_a = a; // 正确:左值引用绑定左值(ref_a 是 a 的别名)ref_a = 20; // 等价于 a = 20,a 现在是 20

2.2 左值引用的限制

  • 不能绑定右值

    // int& ref = 10; // 错误:左值引用不能绑定右值(10 是右值)
    
  • const 左值引用是特例:可以绑定右值(临时延长右值的生命周期):

    const int& ref = 10; // 正确:const 左值引用可绑定右值
    // 原理:编译器会生成临时变量存储 10,ref 绑定这个临时变量(临时变量生命周期被延长至 ref 相同)
    

2.3 左值引用的用途

  • 避免函数传参时的拷贝(如传递大对象 vector):

    void func(const vector<int>& v) { ... } // 传引用,无拷贝
    
  • 允许函数修改外部变量(非 const 引用):

    void increment(int& x) { x++; }
    int a = 5;
    increment(a); // a 变为 6
    

3、右值引用:绑定右值的 “专属引用”

右值引用是 C++11 新增的引用类型,用 && 表示,专门绑定右值,目的是 “利用右值的临时特性” 实现移动语义(避免不必要的拷贝)。

3.1 基本用法

int&& ref1 = 10; // 正确:右值引用绑定纯右值(10 是右值)int a = 10, b = 20;
int&& ref2 = a + b; // 正确:右值引用绑定表达式结果(右值)

3.2 右值引用的限制

  • 不能直接绑定左值:

    int a = 10;
    // int&& ref = a; // 错误:右值引用不能直接绑定左值
    
  • 但可以通过 std::move 将左值 “强制转换” 为右值引用(本质是告诉编译器:“这个左值可以被当作右值处理,资源可以被转移”)

    int a = 10;
    int&& ref = std::move(a); // 正确:std::move 将 a 转为右值引用
    

    注意std::move 不会移动任何数据,只是 “标记” 左值为 “可被移动” 的右值,本身是编译期操作,无运行时开销。

3.3 右值引用的核心用途:移动语义

右值引用的最大价值是实现移动语义—— 对于临时对象(右值),不再进行昂贵的拷贝,而是直接 “窃取” 其资源(如内存),大幅提升性能。

移动构造函数

class MyString {
private:char* data; // 存储字符串的动态内存
public:// 普通构造函数MyString(const char* str) {size_t len = strlen(str);data = new char[len + 1];strcpy(data, str);cout << "构造函数:分配内存" << endl;}// 拷贝构造函数(左值引用参数,深拷贝)MyString(const MyString& other) {size_t len = strlen(other.data);data = new char[len + 1];strcpy(data, other.data);cout << "拷贝构造:深拷贝(性能差)" << endl;}// 移动构造函数(右值引用参数,直接窃取资源)MyString(MyString&& other) noexcept {data = other.data; // 直接接管 other 的内存other.data = nullptr; // other 放弃资源(避免析构时重复释放)cout << "移动构造:窃取资源(性能好)" << endl;}~MyString() {if (data) delete[] data;cout << "析构函数:释放内存" << endl;}
};int main() {MyString s1("hello"); // 调用普通构造MyString s2 = s1; // 调用拷贝构造(s1 是左值,必须深拷贝)MyString s3 = MyString("world"); // 调用移动构造(临时对象是右值,直接窃取资源)MyString s4 = std::move(s1); // 调用移动构造(s1 被转为右值,资源被 s4 窃取)return 0;
}

输出

构造函数:分配内存
拷贝构造:深拷贝(性能差)
构造函数:分配内存
移动构造:窃取资源(性能好)
移动构造:窃取资源(性能好)
析构函数:释放内存
析构函数:释放内存
析构函数:释放内存

关键:移动构造函数通过右值引用参数,识别出临时对象(或被 std::move 标记的左值),直接接管其资源,避免了拷贝开销(对于大对象,性能提升显著)。

4、万能引用与完美转发

在模板中,还有一种特殊的 “万能引用”,它能同时接受左值和右值,并通过 “完美转发” 保持原表达式的属性(左值还是右值)。

4.1 万能引用:能接受左值和右值的引用

万能引用仅出现在模板参数中,形式为 T&&,其类型会根据传入的参数自动推导:

  • 若传入左值,T&& 会被推导为 “左值引用”(T&);
  • 若传入右值,T&& 会被推导为 “右值引用”(T&&)。
template <typename T>
void func(T&& t) { // 万能引用(仅模板中 T&& 才是万能引用)// 根据传入的参数,t 可能是左值引用或右值引用
}int main() {int a = 10;func(a); // 传入左值,T 推导为 int&,func 实际为 void func(int& t)func(20); // 传入右值,T 推导为 int,func 实际为 void func(int&& t)return 0;
}

注意:万能引用≠右值引用。只有模板中 T&&T 是模板参数时,才是万能引用;其他场景的 && 都是右值引用(如 void func(int&& t) 是右值引用)。

4.2 完美转发:保持原参数的左值 / 右值属性

完美转发是指在函数模板中,将参数通过万能引用接收后,原封不动地转发给其他函数(保持其左值 / 右值属性)。需要配合 std::forward 实现。

为什么需要完美转发?
如果直接传递万能引用参数,会丢失原属性(因为引用本身是左值):

void target(int& x) { cout << "左值版本" << endl; }
void target(int&& x) { cout << "右值版本" << endl; }template <typename T>
void func(T&& t) {target(t); // 错误:t 是引用(有名字),被当作左值处理,始终调用 target(int&)
}int main() {int a = 10;func(a); // 传入左值,期望调用 target(int&) → 实际正确func(20); // 传入右值,期望调用 target(int&&) → 实际错误(调用了左值版本)return 0;
}

std::forward 实现完美转发

template <typename T>
void func(T&& t) {target(std::forward<T>(t)); // 完美转发:保持 t 的原始属性
}int main() {int a = 10;func(a); // 传入左值 → 转发为左值 → 调用 target(int&)func(20); // 传入右值 → 转发为右值 → 调用 target(int&&)return 0;
}

原理std::forward<T>(t) 会根据 T 的类型(左值引用或右值引用),将 t 还原为原始的左值或右值属性。

5、常见问题

  1. 左值和右值有什么区别?

    • 左值:有标识符,可以取地址,生命周期较长,可以出现在赋值左边
    • 右值:匿名临时对象,不能取地址,生命周期短,只能出现在赋值右边
  2. 左值引用和右值引用有什么区别?

    • 左值引用 (T&):只能绑定到左值,用于创建别名
    • 右值引用 (T&&):只能绑定到右值,用于实现移动语义和完美转发
  3. std::move 做了什么?它真的移动数据吗?

    • std::move 只是进行类型转换,将左值转换为右值引用
    • 它本身不移动任何数据,只是标记对象为"可移动的"
    • 实际的移动操作在移动构造函数移动赋值运算符中完成
  4. 使用 std::move 后,原始对象会怎样?

    • 对象处于"有效但未指定状态"
    • 不应该再使用该对象,除非重新赋值
    • 析构函数仍然需要正常工作
  5. 移动语义有什么优势?

    1. 性能提升:避免不必要的深拷贝,特别是对于管理资源的类
    2. 资源转移:允许高效地转移资源所有权
    3. 支持不可拷贝对象:可以移动但不能拷贝的对象
  6. std::forwardstd::move 有什么区别?

    • std::move:无条件将左值转为右值引用
    • std::forward:有条件地转换,保持参数的原始值类别
  7. 如何实现一个支持移动语义的字符串类?

    class MyString {
    private:char* data;size_t size;public:// 移动构造函数MyString(MyString&& other) noexcept : data(other.data), size(other.size) {other.data = nullptr;other.size = 0;}// 移动赋值运算符MyString& operator=(MyString&& other) noexcept {if (this != &other) {delete[] data;data = other.data;size = other.size;other.data = nullptr;other.size = 0;}return *this;}// 析构函数~MyString() {delete[] data;}// 禁用拷贝(可选)MyString(const MyString&) = delete;MyString& operator=(const MyString&) = delete;
    };
    
  8. 什么情况下应该使用完美转发?

    应该在包装函数、工厂函数、构造函数转发等场景中使用完美转发,以保持参数的原始值类别。

    // 包装器函数
    template<typename Func, typename... Args>
    auto wrapper(Func&& func, Args&&... args) {return std::forward<Func>(func)(std::forward<Args>(args)...);
    }// 工厂函数
    template<typename T, typename... Args>
    T create(Args&&... args) {return T(std::forward<Args>(args)...);
    }
    
  9. 右值引用本身是左值还是右值?

    右值引用本身是左值。因为右值引用有名字、可以取地址(符合左值的特征)。

    int&& ref = 10; // ref 是右值引用,但自身是左值
    int& ref2 = ref; // 正确:ref 是左值,可绑定到左值引用
    

    这也是为什么在转发右值引用时,需要用 std::forward 才能还原其右值属性。

  10. const 左值引用为什么能绑定右值?

    为了灵活性。const 左值引用设计的初衷之一是 “安全地引用临时对象”,编译器会为右值创建一个临时变量,const 左值引用绑定这个临时变量,并延长其生命周期(与引用同生命周期)。

    用途:允许函数接受右值作为参数(如 void print(const string& s) 可接收字符串字面量 print("hello")),同时保证不修改原对象(const 约束)。

  11. 什么时候该用移动语义?

    1. 函数返回局部对象时
    2. 传递临时对象给函数时
    3. 容器重新分配内存时
    4. 资源管理类(如智能指针)传递所有权时
  12. 什么是返回值优化(RVO)?与移动语义的关系?

    返回值优化(RVO)是编译器优化技术,允许直接在调用者内存中构造返回对象,避免拷贝。与移动语义的关系:

    1. RVO优先级高于移动语义
    2. 当RVO不可用时,编译器使用移动语义
    3. C++17强制要求部分场景的RVO(称为"guaranteed copy elision")

6、总结

左值/右值
├── 左值 (lvalue):有名字、可寻址
├── 右值 (rvalue):临时对象、字面量
│   ├── 纯右值 (prvalue)
│   └── 将亡值 (xvalue)
│
引用类型
├── 左值引用 (&):绑定左值
├── 右值引用 (&&):绑定右值
└── 通用引用 (T&&):模板推导└── 完美转发 (std::forward)
http://www.hskmm.com/?act=detail&tid=9736

相关文章:

  • 基数排序模板(Radix Sort)
  • [项目开发经验分享]基于强类型事件的类型参数传递问题 —— 在 .NET Winform项目中如何设计泛型事件总线以实现UI与核心层的解耦
  • python3安装pip3
  • 堆基础知识
  • RUST 实现 Future trait
  • 行程长度编码
  • mysql 虚拟列,可以简化 SQL 逻辑、提升查询效率
  • Flash Attention算法动画
  • PointNetwork-求解TSP-05 - jack
  • 多站点的TSP问题求解-06 - jack
  • Windows 11如何进入安全模式
  • C# CAN通信上位机系统设计与实现
  • 进程池VS线程池
  • 聊聊昨天CodeBuddy Meetup的一些收获与思考
  • 框架的诞生,本就是人类文明共同涌现的结晶,绝不是某个人的独自觉悟
  • python+Django开发笔记(结合禅道开发测试报告)
  • MVC分层设计模式 2章
  • Questions about learning Symfony
  • 【Python】cx_Freeze模块_打包exe
  • ctfshow web22(子域名爆破)
  • PLC中的运动控制 - (一)轴
  • 墨者学院 某防火墙默认口令
  • IOC控制反转的解耦(相比于直接new对象的正向控制)
  • 墨者学院 浏览器信息伪造
  • AT_arc156_c [ARC156C] Tree and LCS
  • 完整教程:ARM指令集总结
  • 封神台 第二章:遇到阻难!绕过WAF过滤
  • 封神台 第三章:为了更多的权限!留言板!
  • 第一篇
  • C#开发ONVIF客户端与RTSP播放库指南