资讯动态

c/c++中的左值右值详解

发布时间:2026/10/26 19:16:56 来源:尧图企业网站定制
前言「左值」「右值」是 C 里被提到最多、被讲清最少的两个词。很多教程用「等号左边是左值,右边是右值」来解释,这个说法在 C 里勉强够用,在 C11 之后完全不够:等号左边也可以是右值引用,函数实参、返回值、成员访问都各有自己的值类别。真正决定一个表达式能做什么的,是它的值类别(value category):能不能取地址、能不能被移动、绑定到哪种引用、decltype推导出什么类型。C17 把值类别整理成三个基础类别和两个组合类别:左值(lvalue)、将亡值(xvalue)、纯右值(prvalue);左值与将亡值合称泛左值(glvalue),将亡值与纯右值合称右值(rvalue)。理解这套分类,才能解释为什么std::move不移动任何东西、为什么具名的右值引用在下一行就变成左值、为什么decltype(x)与decltype((x))结果不同。最常见的误解有三个。第一,认为std::move(x)会执行移动——它只做一次类型转换,把表达式变成将亡值,真正的移动发生在被调用的移动构造或移动赋值里。第二,认为声明为T的变量就是右值——变量名本身永远是左值,哪怕它的类型是右值引用。第三,认为右值引用一定能延长生命周期——绑定到临时对象的右值引用确实会延长,但从函数返回右值引用就是悬垂。本文按「值类别、引用绑定、move 与 forward、完整示例」展开,最后列出常见坑点。示例以 C17 为基准。一、三个基础类别左值(lvalue)有身份(identity,能取地址、能比较地址),且不能被隐式移动。变量名、解引用表达式*p、数组下标a[i](内置类型)、前置自增的结果,都是左值。纯右值(prvalue)没有身份,用于初始化对象或参与运算。字面量42、a b的结果、按值返回的函数调用结果,都是纯右值。纯右值不能取地址,42不合法。将亡值(xvalue)有身份,但可以被移动。它通常由std::move(x)、static_castT(x)产生,或者是对将亡值的成员访问。将亡值的存在,是为了让「我确定不再需要这个对象了」这件事能在类型系统里表达出来。组合关系是:glvalue lvalue xvalue;rvalue xvalue prvalue。判断一个表达式,先问「有没有身份」,再问「能不能被移动」,就能定位它的类别。decltype对值类别很敏感。对未加括号的标识符,decltype(x)给出声明时的类型;对加了括号的表达式,decltype((x))按值类别给引用:左值给T,将亡值给T,纯右值给T。这也是模板库中判断「表达式类别」的常用手段。二、引用绑定规则左值引用T只能绑定左值;const T可以绑定左值,也可以绑定右值,绑定右值时临时对象的生命周期延长到该引用的生命周期结束。右值引用T只能绑定右值(将亡值或纯右值);它可以绑定临时对象并延长其生命周期。函数重载时,编译器按实参的值类别选择:f(T)接受左值,f(const T)接受左值与右值,f(T)接受右值。同时提供f(const T)与f(T)时,右值优先选后者,这就是「移动语义」在接口层的体现。模板里的T是另一回事。当T需要推导时(函数模板参数、auto),T是转发引用(forwarding reference),它能绑定左值也能绑定右值,具体类型由实参决定:传左值时T推导为U,经过引用折叠得到U;传右值时T推导为U,得到U。引用折叠规则是: , , , 。只有「唯一一个」的场合才得到左值引用。注意,类模板成员函数里的T不一定是转发引用,因为T在类实例化时已经确定,不再参与推导。判断标准是「是否发生类型推导」,不是「长得像不像T」。三、std::move与std::forwardstd::move(x)的定义等价于static_caststd::remove_reference_tdecltype(x)(x):它把任意表达式转成将亡值,本身不读取、不复制、不释放任何资源。真正「搬走」资源的是接收这个将亡值的移动构造函数或移动赋值运算符。若类型没有移动操作,重载解析会退回拷贝,此时std::move只是让拷贝发生,没有任何收益。对const对象调用std::move,得到的是const T。const T不能绑定到T形参,只能绑定到const T,于是调用的是拷贝构造。这是「对 const 对象 move 会退化成拷贝」的原因。std::forwardT(x)用于转发引用:当T是左值引用类型时,它返回左值;当T是普通类型时,它返回右值。它的作用是「保持调用方传入的值类别」,而不是「无条件转成右值」。在泛型包装函数里,参数要一路std::forward下去,否则具名的T参数在函数体内是左值,右值语义在这里断掉。被移动后的对象处于「有效但未指定」(valid but unspecified)状态:可以析构、可以重新赋值,但不要读取它的值,也不要假设它为空。四、完整示例下面的示例演示值类别的判定、重载选择、转发引用与decltype的差异。// 适用于 C17#include iostream#include string#include type_traits#include utilityvoid take(const std::string s) {std::cout const lvalue ref: s \n;}void take(std::string s) {std::cout rvalue ref: s \n;}// 转发引用:保持调用方的值类别template typename Tvoid relay(T value) {take(std::forwardT(value));}int main() {std::string a hello;take(a); // 左值,选 consttake(std::string(tmp)); // 纯右值,选 take(std::move(a)); // 将亡值,选 const std::string c const;take(std::move(c)); // const T 只能绑 const,退化std::string b world;relay(b); // T std::string,转发为左值relay(std::string(rvalue)); // T std::string,转发为右值relay(std::move(b)); // T std::string,转发为右值;b 之后不再使用int n 0;int r n;static_assert(std::is_same_vdecltype(n), int);static_assert(std::is_same_vdecltype((n)), int);static_assert(std::is_same_vdecltype(r), int);static_assert(std::is_same_vdecltype(std::move(n)), int);static_assert(std::is_same_vdecltype(n 1), int);std::cout n r \n;return 0;}按标准语义推演:take(a)选const;take(std::string(tmp))与take(std::move(a))选;take(std::move(c))中实参类型是const std::string,不能绑定到std::string,只能绑定const std::string,所以仍走左值重载;relay(b)中T为std::string,转发后是左值;两个右值实参转发后走重载;decltype的四条断言按值类别规则都成立。以上只按语言规则推导,未经编译运行核对。五、值类别对照类别有身份可被移动能否取地址典型表达式左值 lvalue是否(需显式 move)能变量名、*p、i将亡值 xvalue是是能std::move(x)、static_castT(x)纯右值 prvalue否是不能42、a b、按值返回的调用泛左值 glvalue是视具体类别能左值与将亡值右值 rvalue否或视情况是视具体类别将亡值与纯右值常见坑点一、以为std::move会移动对象❌std::move(v);单独一行,以为v的资源已经被搬走。✅std::move只做类型转换;必须把它用在移动构造或移动赋值的实参位置,例如T t std::move(v);。二、对const对象使用std::move期望移动❌const std::string s x; std::string t std::move(s);实际调用拷贝构造。✅ 需要移动就不要声明为const;const T无法绑定T形参。三、具名右值引用在函数体内当右值用❌void f(T x) { g(x); }——x是左值,调用的是g(T)或g(const T)。✅ 用g(std::forwardT(x))转发,或者明确g(std::move(x))表示不再使用。四、移动后继续读取对象的值❌std::string t std::move(s); std::cout s;读取处于「有效但未指定」状态的对象。✅ 移动后只允许析构或重新赋值;需要保留内容时先拷贝。五、返回局部变量时画蛇添足地std::move❌return std::move(local);阻止了返回值优化,强制一次移动构造。✅ 直接return local;,编译器可以省略拷贝/移动(C17 对纯右值返回有强制省略的情形)。六、把类模板成员里的T当转发引用❌ 在template typename T class Box { void set(T v); };中认为T能自动转发。✅T在类实例化时已确定,不参与推导;需要转发时把成员函数本身写成模板,或按值/按const接收。七、返回右值引用导致悬垂❌T f() { T local; return std::move(local); }返回局部对象的右值引用,调用方拿到即悬垂。✅ 按值返回;右值引用形参/返回值只用于「引用已有对象」的场景。八、对字符串字面量使用std::move❌std::move(abc)得到const char()[4],既不能移动也没有意义,还容易让重载解析出意外结果。✅ 字面量直接传递;需要std::string时显式构造。总结问题答案左值和右值的区别是什么左值有身份且不可隐式移动;右值可被移动,其中将亡值有身份、纯右值没有std::move做了什么把表达式转成将亡值,不移动任何资源为什么T参数在函数内是左值具名变量永远是左值,类型不影响这一点什么时候用std::forward转发引用参数需要保持调用方的值类别时为什么对const对象 move 会拷贝const T只能绑定const Tdecltype(x)与decltype((x))为何不同前者取声明类型,后者按表达式的值类别给引用左值与右值的核心只有一句话:值类别描述的是「表达式」,不是「变量」;类型是T的变量名,作为表达式仍然是左值。把这句话记住,再理解std::move是转换、std::forward是保持,移动语义的绝大部分困惑就会消失。

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

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

免费获取报价 →
↑