资讯动态

C++左值右值解析:从基础到移动语义

发布时间:2026/10/21 2:30:28 来源:尧图企业网站定制
1. 是什么左值lvalue有名称、有持久存储的对象可以取地址可出现在赋值号左边。右值rvalue临时的、无名称的值不能取地址只能出现在赋值号右边。C11 将其细化为 lvalue / xvalue / prvalue 三分类完整体系见 7.2。核心判定规则能对表达式取地址吗能 → 左值不能 → 右值。int a 10, b 20; a 20; // a 是左值有名字、有地址、能被赋值 a; // 正确能取地址 10 a; // 错误字面量 10 是右值 // 10; // 错误不能取地址 a b; // 右值表达式临时结果 // (a b); // 错误不能取地址2. 值类别完整体系C11 三分类C11 起表达式的值类别由传统的“左值 / 右值”二分法细化为三分类类别全称一句话定义lvaluelocator value左值有身份可取地址、可复用的对象xvalueexpiring value亡值有身份、但资源可被窃取的“将亡”对象prvaluepure rvalue纯右值无身份的临时值字面量、表达式计算结果三分类通过两个组合概念与传统的二分法衔接glvalue泛左值 lvalue xvalue → 判定标准有身份能取地址 rvalue右值 xvalue prvalue → 判定标准可被移动资源可窃取 ┌────────── glvalue ──────────┐ │ │ lvalue xvalue ──┐ │ │ ├── rvalue └─────────────────────────────┘ │ prvalue关键结论xvalue 同时属于 glvalue有身份和 rvalue可移动——它就是二分法中“有身份的右值”。3. 亡值 xvalue 专项是什么亡值是有名字或有地址可追踪、但生命周期即将结束的对象。它“将亡”所以资源可以被安全偷走它“有身份”所以仍能区分自己与其他对象。设计目的编译器需要精确回答一个问题——“这个对象之后还会被使用吗”对 lvalue不能偷资源之后可能还要用对 prvalue直接整体当作结果无需区分对 xvalue资源可偷——这是移动语义的触发开关。没有 xvalue 这一类别std::move(x)后的x就无法在类型系统层面标记为“可窃取”编译器无法安全选择移动构造而非拷贝构造。产生亡值的三个典型场景#include utility #include string std::string make_name(); // 返回右值引用的函数声明 int main() { std::string s hello; // 场景一std::move 的返回值——最常用的亡值来源 std::string r1 std::move(s); // 场景二static_castT 的转换结果——std::move 内部就是这么做的 std::string r2 static_caststd::string(s); // 场景三调用返回 T 的函数较少见标准库内部使用 std::string r3 make_name(); return 0; }识别口诀亡值 有身份 是右值。判定问题lvaluexvalueprvalue有身份可取地址✅✅❌可被移动资源可窃❌✅✅出现在左边✅❌❌注意xvalue 是右值因此不能取地址std::move(s)非法也不能出现在赋值号左边它只是“语义上”拥有身份——绑定它的右值引用仍指向那个真实存在的对象。4. 特性对比特性lvaluexvalueprvalue名称有无名字但有对象无临时存储持久作用域内有效即将销毁临时语句结束即销毁取地址✅❌❌出现在左边✅❌❌可绑定到T✅❌❌可绑定到T❌✅✅资源可被窃取❌✅✅5. 表达式中的行为差异int a 10, b 20; a b; // b 是左值但在此处作为右值使用读取它的值 // 左值在“需要值”的上下文会隐式转换为右值 a b 30; // 错误ab 的结果是右值不能被赋值 a; // a 返回左值直接修改 a 并返回 a 本身 a; // a 返回右值返回 a 的旧值副本临时对象为什么a是左值而a是右值a前置自增先自增返回 a 自身的引用 → 有身份 → 左值。a后置自增先保存旧值副本自增 a返回那个临时副本 → 右值。6. 判定规则汇总表达式值类别原因变量名a左值有名字、可取地址字面量10、c、3.14纯右值无地址字符串字面量除外算术表达式a b纯右值临时结果前置a、*p左值返回对象本身后置a纯右值返回旧值副本函数返回T左值返回对象本身函数返回T按值纯右值临时对象std::move(x)亡值转换为右值引用标记资源可窃取static_castT(x)亡值同上std::move 的本质调用返回T的函数亡值返回“将亡”对象的引用7. 左值引用与右值引用C11int a 10; int lr a; // 左值引用只能绑定左值 // int lr2 10; // 错误不能绑定右值 int rr 10; // 右值引用C11只能绑定右值 // int rr2 a; // 错误不能绑定左值 const int clr 10; // const 左值引用万能绑定左值、右值都行引用类型绑定左值绑定右值T✅❌T❌✅const T✅✅8. 移动语义与 std::move 原理C11 引入右值引用的核心目的是实现移动语义move semantics右值是临时的、即将销毁的其资源可以被“偷走”而不必深拷贝从而大幅提升性能。std::string s1 hello; std::string s2 s1; // 拷贝构造深拷贝字符串内容 std::string s3 s1 s2; // 移动构造s1s2 是右值临时直接偷走资源没有左右值区分编译器就无法判断“这个对象之后还会不会被使用”也就无法安全地做移动优化。std::move 的本质一个模板函数只做类型转换把左值标记为亡值不移动任何数据。简化后的源码// std::move 的简化实现位于 utility摘除引用折叠细节 templatetypename T constexpr std::remove_reference_tT move(T t) noexcept { // 关键就这一行无条件转换为右值引用类型 return static_caststd::remove_reference_tT(t); }以std::move(s)s 是std::string左值为例的展开过程第一步模板推导 T std::string左值传入时 T 推导为左值引用 第二步去引用 remove_reference_tT std::string 第三步类型转换 static_caststd::string(s) → 表达式类别变为亡值 xvalue 第四步返回 返回 std::string 类型的亡值转换前后发生了什么阶段s 的值类别编译器视角转换前左值lvalue“s 之后还要用不能偷它的资源”转换后亡值xvalue“调用者承诺不再使用 s 的资源可以偷”std::string s hello; std::string t std::move(s); // 触发移动构造偷走 s 的内部缓冲区 // 此时 s 处于“有效但未定义”状态通常为空不要再读它的值设计要点noexcept修饰移动操作必须保证不抛异常std::vector扩容时才会放心用移动而非拷贝。constexpr允许在编译期常量表达式中使用。remove_reference_t保证无论传入左值还是右值返回的都是“去掉引用后再加上”的干净右值引用类型。9. 注意事项有名字的右值引用变量本身是左值int rr 10;中rr有名字、可取地址是左值要再次触发移动需std::move(rr)。std::move只做类型转换左值 → 亡值本身不移动任何东西。被 move 后的对象仍可安全析构移动构造/移动赋值必须保证源对象处于“有效但未定义”状态通常为空不要读取其值但可以给它赋新值。亡值不能取地址std::move(x)编译错误——亡值是右值“有身份”指的是其引用目标真实存在不代表表达式本身可取地址。区分值类别的本质是判断这个对象之后还会不会被使用。不会 → 可以安全移动标记为亡值。10. 常考面试题以下题目围绕左值、右值、引用以及移动语义展开是 C 面试中的高频考点。10.1 如何一句话区分左值和右值核心判断能否对表达式取地址。能取地址、有名字的是左值不能取地址、临时的是右值。C11 后右值进一步细分为 xvalue 和 prvalue。10.2 为什么需要引入右值引用为了实现移动语义。右值通常是临时对象资源即将销毁可以安全地把内部资源转移给新对象避免深拷贝从而提升性能。10.3 std::move 真的移动数据吗不会。std::move只做类型转换把参数无条件转换为右值引用使表达式变成亡值从而让编译器选择移动构造或移动赋值。真正的资源转移发生在移动构造函数或移动赋值运算符中。// std::move 的核心等价操作 templatetypename T std::remove_reference_tT move(T t) noexcept { return static_caststd::remove_reference_tT(t); }10.4 被 std::move 后的对象还能使用吗对象处于“有效但未定义”状态可以安全析构也可以重新赋值但不应继续读取其原有值。例如std::string被移动后通常为空。10.5 为什么说有名字的右值引用变量本身是左值int rr 10;中rr有名字、可以取地址因此rr本身是左值。若想再次触发移动语义需要写成std::move(rr)。10.6 为什么前置 返回左值后置 返回右值前置a直接修改对象并返回自身引用所以是左值后置a先保存旧值副本再返回这个临时副本所以是右值。10.7 移动构造函数有哪些关键点通常声明为noexcept保证标准容器扩容时优先使用移动。转移堆资源、指针等内部资源后把源对象置回“有效但未定义”状态如将指针置空。移动构造不会分配新资源复杂度通常为 O(1)。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑