作为C/C++开发人员,在平时的项目开发过程中,或多或少的听过左值和右值的概念,甚至在编译器报错的时候,遇到过lvalue
和rvalue
等字样;甚至使用过std::move(),但是不知道其含义。作为多年的C++开发人员,一直以来,对左值右值的理解没有一个系统的认识,总感觉似懂非懂。今天,借助本文,详细的介绍下这些知识点,并从代码实例的角度去分析什么是左值或者右值,同时,也算是给自己知识点做一个总结。
作为C++开发人员,相信我们都写过如下代码:
void fun(int &x) {
//
}
int main() {
fun(10);
return 0;
}
在编译的时候,会提示如下:
error: invalid initialization of non-const reference of type ‘int&’ from an rvalue of type ‘int’
其中上述报错中的rvalue就是10,也就是说10就是rvalue,那么到底什么是rvalue,rvalue的意义是什么?这就是本文的目的,通过本文,让你彻底搞清楚什么C++下的值类别,以及如何区分左值、纯右值和将亡值。
本文的主要内容如下图所示:
在正式介绍左值和右值之前,我们先介绍下其历史。
编程语言CPL第一次引入了值类别,不过其定义比较简单,即对于赋值运算符,在运算符左边的为左值,在运算符右边的为右值。
C语言遵循与CPL类似的分类法,但是弱化了赋值的作用,C语言中的表达式被分为左值
和其它(函数和非对象值),其中左值被定义为标识一个对象的表达式。不过,C语言中的左值与CPL中的左值区别是,在C语言中lvalue是locator value
的简写,因此lvalue对应了一块内存地址。
C++11之前,左值遵循了C语言的分类法,但与C不同的是,其将非左值表达式统称为右值,函数为左值,并添加了引用能绑定到左值但唯有const的引用能绑定到右值的规则。几种非左值的C表达式在C++中成为了左值表达式。
自C++11开始,对值类别又进行了详细分类,在原有左值的基础上增加了纯右值和消亡值,并对以上三种类型通过是否具名(identity)和可移动(moveable),又增加了glvalue和rvalue两种组合类型,在后面的内容中,会对这几种类型进行详细讲解。
C/C++代码是由标识符、表达式和语句以及一些必要的符号(大括号等)组成。
表达式由按照语言规则排列的运算符,常量和变量组成。一个表达式可以包含一个或多个操作数,零个或多个运算符来计算值。每个表达式都会产生一些值,该值将在赋值运算符的帮助下分配给变量。
在C/C++中,表达式有很多种,我们常见的有前后缀表达式、条件运算符表达式等。字面值(literal)和变量(variable)是最简单的表达式,函数的返回值也被认为是表达式。
表达式是可求值的,对表达式求值可得到一个结果,这个结果有两个属性:
在上节中,我们提到表达式是可求值的,而值类别就是求值结果的属性之一。
在C++11之前,表达式的值分为左值和右值两种,其中右值就是我们理解中的字面值1、true、NULL等。
自C++11开始,表达式的值分为左值(lvalue, left value)
、将亡值(xvalue, expiring value)
、纯右值(pvalue, pure ravlue)
以及两种混合类别泛左值(glvalue, generalized lvalue)
和右值(rvalue, right value)
五种。
这五种类别的分类基于表达式的两个特征:
结合上述两个特征,对五种表达式值类别进行重新定义:
用图表示如下:
从glvalue和rvalue出发,将具名(indentity)和可移动两个特征结合起来,如下图所示:
在上图中,I代表indentity,M代表moveable。以xvalue为例,在上图中xvalue为(I&M),即代表具名且可移动。
对于indentity,有些文章译为
有身份的
,有些文章译为具名的
,本文统一称为具名的
。
左值(lvalue,left value),顾名思义就是赋值符号左边的值。准确来说,左值是表达式结束(不一定是赋值表达式)后依然存在的对象。
可以将左值看作是一个关联了名称的内存位置,允许程序的其他部分来访问它。在这里,我们将 "名称" 解释为任何可用于访问内存位置的表达式。所以,如果 arr 是一个数组,那么 arr[1] 和 *(arr+1) 都将被视为相同内存位置的“名称”。
左值具有以下特征:
那么哪些都是左值呢?查了相关资料,做了些汇总,基本覆盖了所有的类型:
->
)运算符的结果[]
)为了能够更加清晰地理解左值,我们举例:
int a = 1; // a是左值
T& f();
f();//左值
++a;//左值
--a;//左值
int b = a;//a和b都是左值
struct S* ptr = &obj; // ptr为左值
arr[1] = 2; // 左值
int *p = &a; // p为左值
*p = 10; // *p为左值
class MyClass{};
MyClass c; // c为左值
"abc"
对于一个表达式,凡是对其取地址(&)操作可以成功的都是左值
在前面有提过,自C++11开始,纯右值(pvalue, pure ravlue)相当于之前的右值,那么什么是纯右值呢?
字面值或者函数返回的非引用都是纯右值。
以下表达式的值都是纯右值:
为了加深对右值的理解,下面的例子是常见的纯右值:
nullptr;
true;
1;
int fun();
fun();
int a = 1;
int b = 2;
a + b;
a++;
b--;
a > b;
a && b;
纯右值特征:
将亡值(xvalue, expiring value),顾名思义即将消亡的值,是C++11新增的跟右值引用相关的表达式,通常是将要被移动的对象(移为他用),比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。
将亡值可以理解为通过“盗取”其他变量内存空间的方式获取到的值。在确保其他变量不再被使用、或即将被销毁时,通过“盗取”的方式可以避免内存空间的释放和分配,能够延长变量值的生命期。(通过右值引用来续命)。
xvalue 只能通过两种方式来获得,这两种方式都涉及到将一个左值赋给(转化为)一个右值引用:
static_cast<T&&>(t);
该表达式得到一个 xvalue下面通过几个代码来详细分析什么是将亡值:
std::string fun() {
std::string str;
// ...
return str;
}
std::string s = fun();
在函数fun()中,str是一个局部变量,并在函数结束时候被返回。
在C++11之前,s = fun();会调用拷贝构造函数,会将整个str复制一份,然后把str销毁。如果str特别大的话,会造成大量额外开销。在这一行中,s是左值,fun()是右值(纯右值),fun()产生的那个返回值作为一个临时值,一旦str被s复制后,将被销毁,无法获取、也不能修改。
自C++11开始,引入了move语义,编译器会将这部分优化成move操作,即不再是之前的复制操作,而是move。此时,str会被进行隐式右值转换,等价于static_cast<std::string&&>(str)
,进而此处的 s
会将 foo
局部返回的值进行移动。
无论是C++11之前的拷贝,还是C++11的move,str在填充(拷贝或者move)给s之后,将被销毁,而被销毁的这个值,就成为将亡值。
将亡值就定义了这样一种行为:具名的临时值、同时又能够被move。
泛左值(glvalue, generalized lvalue),又称为广义左值,是具名表达式,对应了一块内存。glvalue有lvalue和xvalue两种形式。
一个表达式是具名的,则称为glvalue,例子如下:
struct S{
int n;
};
S fun();
S s;
s;
std::move(s);
fun();
S{};
S{}.n;
在上述代码中:
glvalue的特征如下:
右值(rvalue, right value)是指可以移动的表达式。prvalue和xvalue都是rvalue,具体的示例见下文。
rvalue具有以下特征:
&1
,&(a + b)
,这些表达式没有意义,也编译不过。3 = 5
,3 += 5
,这些表达式没有意义,也编译不过。const int& a = 1
。经过前面的内容,我们对左值和右值(纯右值和将亡值)有了一个初步的认识,在本节,我们借助一些例子,来加深对左值和右值的理解。
代码如下:
int i = 0;
++i;
--i;
i++;
i--;
在上面代码中,我们定义了一个int类型的变量i,并初始化为0。
代码如下:
int x = 0;
int y = 0;
x + y;
x && y;
x == y;
在上述代码中,x + y得到的是一个不具名的临时对象,所以x+y是纯右值;而x && y和x == y得到的是一个bool常量值,要么是true要么是false,所以是纯右值。
代码如下:
int x = 0;
int *y = &x;
*y = 1;
&y;
*y得到的是y指向地址的实际值,所以&(*y)是合法的,因此*y是左值;对&y操作得到的是一个地址,即一个long值,所以是一个字面值,因此&y是纯右值。
字符串字面值为左值,这个比较特殊。在前面提到过字面值都是纯右值(字符串字面值除外),一个很重要的原因,就是可以字符串字面值可以获取地址
,
下面代码在编译器中可正常编译且运行:
std::cout << &"abc" << std::endl;
这是因为C++将字符串字面值实现为char型数组,实实在在地为每个字符都分配了空间并且允许程序员对其进行操作
。如果从存储区的概念来理解,那就是字符串字面值存储在常量区
。
既然提到了左值右值,就得提一下引用。
在C++11之前,引用分为左值引用和常量左值引用两种,但是自C++11起,引入了右值引用,也就是说,在C++11中,包含如下3中引用:
左值引用和常量左值引用,我们很常见,如下代码:
std::string str = "abc";
std::string &s = str;
const int &a = 10;
int &b = 10; // 错
在上述代码中,s是一个左值引用,而a是一个const 左值引用。那么,为什么最后一句int &b = 10;
编译器会报错呢?这是因为10是常量,而常量是右值,一个右值怎么能够被左值引用去引用呢。
那么什么是右值引用呢?右值引用就是引用右值的引用,这不废话嘛。
在C++11中引入了右值引用,因为右值的生命周期很短,右值引用的引入,使得可以延长右值的生命周期。在C++中规定,右值引用是&&即由2个&表示,而左值引用是一个&表示。右值引用的作用是为了绑定右值
。
为了能区分左值引用和右值引用,代码如下:
int a = 1;
int &rb = a; // b为左值引用
int &&rrb = a; // 错误,a是左值,右值引用不能绑定左值
int &&rrb1 = 1; // 正确,1为右值
int &rb1 = i * 2; // 错误,i * 2是右值,而rb1位左值引用
int &&rrb2 = i * 2; // 正确
const int &c = 1; // 正确
const int &c1 = i * 2; // 正确
在这里,我们需要特别注意的一点就是右值引用虽然是引用右值,但是其本身是左值
,以下代码为例:
int &&a = 1;
在上述代码中,a是一个右值引用,但是其本身是左值,合适因为:
我们在前面有提到过,一个表达式有两个属性,分别为类型和值类别。本节说的左值引用和右值引用就属于类型
,而左值和右值则属于值类别范畴
,这个概念很重要,千万不能混淆。
可能有人会问,除了自己根据规则区分左值引用和右值引用,有没有更快更准确的方式来判断呢?其实,系统提供了API,如下:
std::is_lvalue_reference
is_rvalue_reference
int a = 1;
int &ra = a;
int &&b = 1;
std::cout << std::is_lvalue_reference<decltype(ra)>::value << std::endl;
std::cout << std::is_rvalue_reference<decltype(ra)>::value << std::endl;
std::cout << std::is_rvalue_reference<decltype(b)>::value << std::endl;
输出结果:
1
0
1
这篇文章是在整理了大量资料,结合自己的理解之后完成的。左值右值这种本身就比较抽象,在写文的过程中,发现有些东西,很难用文字来描述。在写这篇文章的过程中,也纠正了自己长久以来对左值右值的疑惑,因为这块确实比较复杂,所以文章中难免有出错或者不周全的地方,希望您批评指正。
好了,今天的文章就到这里,我们下期见!
https://en.cppreference.com/w/cpp/language/value_category
https://www.internalpointers.com/post/understanding-meaning-lvalues-and-rvalues-c https://www.fatalerrors.org/a/left-value-reference-and-right-value-reference-of-c-c-class-and-object.html
https://www.bogotobogo.com/cplusplus/C11/4_C11_Rvalue_Lvalue.php
https://users.soe.ucsc.edu/~pohl/code/lvalue.htm
本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/_9-0iNUw6KHTF3a-vSMCmg
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。