左值、右值、纯右值、将亡值:值类别入门

发布时间:2026/10/5 6:52:26

左值、右值、纯右值、将亡值:值类别入门 你有没有写过std::move(x)之后发现它还能被读取或者重载了f(T)和f(T)却搞不清到底调了哪个这些怪现象的根都在「值类别value category」上。C11 起给每个表达式贴了一张隐形标签它直接决定一个表达式能不能取地址、能不能被移动、以及重载决议时该选哪个版本。这篇把五个概念一次讲清并用真跑出来的输出告诉你每种实参选中了哪个函数。官方文档Value categories (cppreference)同一个名字两种命运先看一个会让人困惑的现象// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includestring#includeutilityvoidwho(conststd::string){std::cout走了 const T\n;}voidwho(std::string){std::cout走了 T\n;}voidwho(std::string){std::cout走了 T\n;}intmain(){std::string s{喵};who(s);// 传变量who(std::move(s));// 传「被移动」的 sreturn0;}走了 T 走了 T同一个s直接传和用std::move包一下调的就不是同一个函数。差别不在变量本身而在「这个实参表达式的值类别」变了。下面拆开这张标签。五个概念与一张分类图C11 之后表达式的值类别value category只有三种基本类型再归成两个集合左值lvalue有名字、可取地址的对象。比如变量名s、解引用*p、i的返回值。纯右值prvaluepure rvalue纯粹的「值」没有身份、不能被取地址。比如字面量42、临时对象std::string{t}、表达式a b的结果。将亡值xvalueeXpiring value本来是个左值但明确「我马上要被搬空了」比如std::move(x)的结果、static_castT(x)的结果。它既是泛左值有身份又是右值可移动两个集合里都有它。两个集合是这么划的泛左值 glvalue lvalue xvalue有身份、可被寻址语义上存在的对象右值 rvalue prvalue xvalue可以被移动走C 表达式 (expression) ┌───────────────────┴───────────────────┐ glvalue rvalue (泛左值有身份) (右值可被移动) ┌────────────┐ ┌────────────┐ lvalue xvalue xvalue prvalue (左值) (将亡值) (将亡值) (纯右值) 变量名/ std::move(x) 将亡值 42 / 临时对象 *p / i static_castT(x) 同时属于 a b └──── xvalue 同时落在两边 ────┘ 字面量官方文档reference collapsing 与值类别的关系 (Core Guidelines)怎么判断一个表达式是左值还是右值记住两条够用的经验法则有名字的变量本身是左值即使它的类型是右值引用T见下一篇。能不能取地址能取地址的通常是左值取不了地址的42、std::move(x)、临时对象是右值。下面这张表把三者对上号特征左值 lvalue纯右值 prvalue将亡值 xvalue可取地址能不能是右值不能是右值有身份identity有无有但仍属右值典型来源具名变量、*p、前置字面量、临时对象、abstd::move(x)、static_castT(x)注意 xvalue 是「有身份但不可取地址」的特殊存在它身份上指向某个具体对象但作为右值表达式std::move(x)是编译不过的。值类别如何决定重载决议核心这是理解移动语义最关键的一步。f(T)/f(const T)/f(T)三个重载谁被选中完全由实参的值类别决定。让每个重载打自己的标签传四种不同值类别的实参看真跑的是谁// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includestring#includeutilityvoidclassify(conststd::string){std::cout[const T] 只读/转发引用\n;}voidclassify(std::string){std::cout[T] 左值引用\n;}voidclassify(std::string){std::cout[T] 右值引用\n;}intmain(){std::string name{喵喵};conststd::string cname{只读};classify(name);// 左值 - Tclassify(cname);// const 左值 - const Tclassify(std::move(name));// 将亡值(xvalue) - Tclassify(std::string{临时});// 纯右值(prvalue) - Treturn0;}[T] 左值引用 [const T] 只读/转发引用 [T] 右值引用 [T] 右值引用规律很清楚左值有名字的普通变量只能绑T或const T优先选非 const 的T。const 左值绑不上T落到const T。右值prvalue 或 xvalue优先绑T只有在没有T重载时才会退化去绑const T。这就是为什么std::move(x)之后调的是T版本——std::move把左值「抬」成了 xvalue于是移动语义的重载被选中。值类别就是重载决议的裁判。值类别决定「拷贝」还是「移动」光看选中哪个重载还不够得看它的实际后果。往std::vector里塞对象时左值走拷贝构造、右值走移动构造。给Blob加上构造/拷贝/移动的日志一眼看清// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includestring#includeutility#includevectorstructBlob{std::string name;explicitBlob(std::string n):name{std::move(n)}{std::cout构造 name\n;}Blob(constBlobo):name{o.name}{std::cout拷贝 name\n;}Blob(Blobo)noexcept:name{std::move(o.name)}{std::cout移动 name\n;}};intmain(){std::vectorBlobv;v.reserve(4);Blob a{左值};v.push_back(a);// 左值 - 拷贝构造v.push_back(std::move(a));// 将亡值 - 移动构造v.push_back(Blob{临时});// 纯右值 - 移动构造return0;}构造 左值 拷贝 左值 移动 左值 构造 临时 移动 临时对照着看a作为左值push_back触发「拷贝」对象被复制一份std::move(a)把它变成将亡值后触发「移动」资源被搬走而不是复制Blob{临时}是纯右值临时对象同样走「移动」。值类别不同编译器生成的代码就不同这正是移动语义能提速的根本原因。看这类日志别只数行数真正要看的是「拷贝」和「移动」这两个词。易错点那些「看起来是右值」其实是左值值类别最容易翻车的地方是「凭直觉猜」。把常见表达式的真实值类别列成一张表比反复背定义管用表达式值类别说明xx的类型是T左值有名字就是左值类型是右值引用也改变不了这点std::move(x)将亡值只是static_castT(x)本身不搬移任何东西abc左值字符串字面量是const char[4]数组数组是左值42/3.14纯右值字面量没有身份i左值返回自增后的对象本身i纯右值返回自增前的旧值副本*p左值解引用拿到的是所指的那个对象x纯右值取地址得到的是一个临时地址值a b纯右值运算产生的是临时对象f()f返回T纯右值C17 起返回值是 prvalue配合强制复制消除f()f返回T左值返回引用时保持被引用对象的值类别x.mm是普通成员与x同类别std::move(x).m是将亡值前两条合起来解释了一个经典困惑为什么std::move(x)之后x还能被读取很多人以为move会当场把东西搬走其实它连一个对象字段都没碰。它只做了一次类型转换x那个对象一个字节都没动真正「搬空」它的是接收方——实参被T重载绑定后接收方内部才动手把资源偷走。记住这个分工std::move就不再有神秘感了。还有个更隐蔽的坑形参的类型是右值引用时它在函数体里是左值。看下面这个程序// demo.cpp — 编译: g -stdc17 -Wall -O2 demo.cpp -o demo#includeiostream#includestring#includeutilityvoidsink(std::string){std::coutsink(T) 走拷贝路径\n;}voidsink(std::string){std::coutsink(T) 走移动路径\n;}voidrelay(std::strings){// 形参 s 的类型是右值引用sink(s);// 但 s 有名字 - 它本身是左值 - 拷贝路径sink(std::move(s));// 显式转回将亡值 - 才走移动路径}intmain(){relay(std::string{临时对象});}sink(T) 走拷贝路径 sink(T) 走移动路径s明明是以右值引用接进来的sink(s)却仍然选了拷贝版本。因为s在relay的函数体里是个有名字的变量值类别是左值。这条规则不是缺陷而是刻意的保护如果「名字是右值引用的变量」自动算右值那么第二次使用s时它可能已经被搬空了编译器无从判断程序会静默出错。想把「可移动」这个信息继续往下传必须显式写std::move(s)或std::forward这正是转发forwarding话题的起点。与移动语义的衔接下一步是 std::forward把这篇的结论串起来移动语义的整条链路就通了值类别是裁判它决定f(T)/f(const T)/f(T)三者谁被选中。std::move(x)是把左值抬成将亡值的类型转换本身不移动任何东西。真正的「搬移」发生在被选中的那个函数体内——比如std::vector的移动构造去偷那块缓冲。于是「什么时候该写std::move」有了明确答案当你确定这个对象之后不再需要、且想交给一个会搬走它的接口时。反过来对一个马上还要继续读的变量写std::move等于把资源提前送人后续读取落在「有效但未指定」的状态上属于自伤。泛型代码里std::move就不够用了模板参数T可能被推导成左值引用也可能是普通类型需要的是「原样保留调用者传进来的值类别」。那是std::forwardT(x)的职责它配合转发引用forwarding reference和引用折叠reference collapsing使用。这一块留到《左值引用右值引用与万能引用》展开这里只需记住结论std::move是无条件转成右值将亡值std::forward是有条件地按原值类别转发把「有名字的变量是左值」这一条钉死两者都不会用错。延伸阅读Value categories (cppreference)最权威的分类定义与边界情形。std::move (cppreference)看清move只是「无条件转成右值引用」不做任何搬移动作。std::forward (cppreference)按原值类别转发的实现与边界。C Core Guidelines · F 章参数与重载值类别在接口设计里的实战约束。收个尾值类别就是 C 给每个表达式贴的隐形标签左值有名字、可取地址右值可被移动将亡值是「马上要被搬空」的那个特殊右值。重载决议选哪个版本、走拷贝还是走移动全看这张标签。先把「有名字的变量就是左值」这一条钉死剩下几个概念基本不会用错。下一篇换引用的视角把值类别和T/T对上。
延伸阅读

更多相关文章

2026/10/5 7:57:28

Nuxt 3/4 路由实现 .html 后缀的三种方案与踩坑记录

做企业官网迁改的时候,甲方那边有一套老业务系统,所有落地页 URL 必须以.html结尾,否则后端解析逻辑直接不认。当时我们正把项目从 Nuxt 3 往 Nuxt 4 升,路由后缀这个需求一来,团队里立刻分成了两派:一派说…

2026/10/5 7:57:28

C#值相等与null==null:运算符重载与Equals实现完整指南

接手这个需求之前, 我先把背景说清楚:我们在 C# 里讲"值相等"(value equality),指的是两个不同对象只要内部关键数据相同,业务上就视为同一个对象;而默认情况下类(class)的…

2026/10/5 7:57:28

TMC stealthChop步进电机静音驱动:原理到寄存器配置全解析

1. 为什么被步进电机噪音烦了这么久,才遇到stealthChop玩步进电机的人应该都有过这种经历:3D打印机一开机,整个房间都是蜂鸣一样的“滋滋”声;CNC雕刻机走直线时,高频啸叫让人戴耳机都嫌吵;做个相机滑轨&am…

2026/10/5 7:57:28

TMC stealthChop步进电机静音驱动原理与STM32配置实战

用过步进电机驱动的朋友应该都有那个瞬间:把板子上的A4988或者DRV8825拆下来,换上TMC2209,通电,电机从那种尖锐的“嘶嘶”声变成几乎只有转子转动的机械声。再开stealthChop,连那点机械声都像是被蒙了一层棉被。这个从…

2026/10/5 7:57:28

26年论文AI率太高实测报告:功能、效果与短板

论文AI率太高,成了这届毕业生躲不过的坎。学校查重系统升级后,AI检测结果直接和答辩资格挂钩,身边好几个同学初稿提交后被判定AI率超40%,被迫连夜返工。市面上号称能降AI的工具不少,但效果参差不齐。我花了两周时间&am…

2026/10/5 7:52:28

MySQL索引失效全解析:8种场景与排查实战

直接开始聊聊索引失效这件事。做后端开发的,就没有不被索引失效坑过的。明明查询走了索引,结果一夜之间接口变慢,或者你可能都不知道那条慢SQL压根儿没用上索引——等DBA找上门的时候,数据量已经涨到几千万,再想改&…

2026/10/5 6:32:56

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑