经常使用但很少定义的术语:lvalue

22

什么是lvalue?


1
相关:https://dev59.com/BnI-5IYBdhLWcg3wCz3y - Roger Pate
8个回答

30

左值(lvalue)是一个可以被赋值的值:

lvalue = rvalue;

它的缩写是“左值”或“左手值”,基本上就是等号=符号左边的值,也就是你要赋值的值。

以下是一个不是左值(即仅为右值)的示例:

printf("Hello, world!\n") = 100; // WTF?

该代码无法工作是因为printf()(返回一个int的函数)不能是左值,只能是右值。


4
+1 很好!"//WTF" 让我笑了!不过,如果 printf 返回一个引用,它将会被视为 lvalue。 - Mehrdad Afshari
那段代码无法工作,因为printf()的返回值是int类型,而不是lvalue。你不能将值赋给函数本身。 - flodin
@flodin - 就我(虽然经验不多)使用C的经历来看,我从未遇到过必须(或者甚至想要)分配函数的情况,但是我在下面的答案中看到了C++中的演示,所以我不会争辩它可能发生。不过,我怀疑这个人很快就不用担心这个问题了。 - Chris Lutz
我花了一个小时来解析和理解WP的“值(计算机科学)”,而实际上我真正需要的是在SO上搜索它! - n611x007
推论是存在一些lvalue,它们无法被分配给其中一个或多个失败的值。 - Antti Haapala -- Слава Україні
显示剩余2条评论

12

出现在赋值语句左侧的东西,即可被赋值的东西。

请注意,在C++中,如果满足以下条件,函数调用可以是lvalue:

int & func() {
   static int a = 0;
   return a;
}

然后:

func() = 42;     // is legal (and not uncommon)

但是这很奇怪,如果你返回一个对象的话可能会破坏程序...这就是为什么很多时候你会看到那里也有一个const。 - Robert P
我看到在C++1x中,他们考虑为隐式operator=添加ref-qualifiers。这意味着对于用户定义的类类型rvalue,将不再允许使用func() = 42;(目前是允许的)。 - Johannes Schaub - litb
1
无论如何,如果你有int*func(){... return &a;},你可以做到: (*func()) = 42; 这是更糟糕的,我认为他们无法阻止你... - Brian Postow

12

传统上来说,“=”操作符左边的是lvalue(可被赋值的值)。然而随着时间的推移,“lvalue”和“rvalue”的含义也有所改变。C++中增加了一个术语叫做“不可修改的lvalue”,它是指任何不能被赋值的lvalue:数组和带有“const”限定符的变量就是其中的两个例子。在C中,你不能对任何rvalue进行赋值(请参见下文)。同样地,在C++中,你不能对不属于某些用户定义的类类型的rvalues进行赋值。

你可以说“lvalue”是一个表达式,它对应着一个随时间持续存在的对象,并占据一些存储位置。你是否能将赋值操作应用于该表达式并不重要,因为这与其分类无关。特别地,一个引用也是一个lvalue,因为它拥有一个随时间持续存在的名字。以下所有内容都是lvalues,因为它们都指向命名的对象。同时请注意,“const”并不会对lvalue性质产生任何影响。

int a; lvalue: a;
       lvalue: ++a;
int a[2]; lvalue: a;
int &ra = a; lvalue: ra;
int *pa = &a; lvalue: *pa;

术语“rvalue”用于诸如文字常量和枚举值以及那些不享受长寿命的临时对象,它们在完整表达式结束时立即被销毁。对于rvalue而言,持久性方面并不重要,而是重视其值方面。C++中的函数是lvalue,因为它们是持久的并且具有地址,尽管它们不是对象。我在上述lvalues概述中将它们省略了,因为首先只考虑对象更容易理解lvalues。以下所有内容都是rvalues:

enum { FOO, BAR }; rvalue: BAR;
int a[2]; rvalue: (a+1);
rvalue: 42;
int a; rvalue: a++; // refering to a temporary
struct mystruct { }; mystruct f() { return mystruct(); } rvalue: f();

顺便提一下,通常情况下您有一个左值,但运算符需要一个右值。例如,二元内置 "+" 运算符添加两个值。左值表达式首先为所有内容指定了一个位置,其中必须首先读取一个值。因此,当您添加两个变量时,会发生“左值到右值”的转换。标准规定,左值表达式中包含的值是其右值结果:

int a = 0, b = 1;
int c = a + b; // read values out of the lvalues of a and b. 

其他运算符不接受右值,只接受左值。它们不读取值。一个例子是取地址运算符 &。你不能对右值表达式取地址。有些右值甚至不是对象:它们不占用任何存储空间。例如字面量(10、3.3等)和枚举值。

那这些可怕的东西有什么用处呢?

嗯,区分左值和右值有几个优点:

  • 允许编译器省略为右值分配内存的过程,并使用寄存器/只读内存来存储标量值
  • 将表达式标记为难以捉摸的: 右值不会长时间存在
    • 允许编译器在内部实现高效的复制语义,在C++11中还向程序员公开了这种技术(请参见移动语义和右值引用):我们可以从即将被销毁的右值中夺取资源
  • 允许基于该属性制定规则
    • 不允许从尚未初始化的对象中生成右值,而左值可以很好地引用未初始化的对象
    • 右值永远不能是多态的。它们的静态类型也必须是它们的动态类型:简化了typeid运算符的规则。

...... 还有更多,我感觉......


+1 是因为提到不能获取 rvalue 的地址。在我看来,这是区分 rvalue 和 lvalue 最简单的方法。 - jweyrich

8

我所知道的最好的解释可以在这篇关于RValue引用的文章中找到。

另一种确定表达式是否为左值的方法是询问“我能取它的地址吗?”。如果可以,那么它就是左值。如果不能,那么它就是右值。例如,&obj,& * ptr,&ptr [index]和&++x都是有效的(即使其中一些表达式很傻),而&1729,&(x + y),&std :: string(“meow”)和&x ++都是无效的。为什么会这样?取地址运算符需要其“操作数应为左值”(C ++ 03 5.3.1 / 2)。为什么它需要呢?取持久对象的地址是可以的,但取临时对象的地址会非常危险,因为临时对象很快就会消失。


虽然这是真的,但它有点自我定义。首先它说:“确定表达式是否为lvalue的另一种方法是问'我可以取它的地址吗?'”然后它说:“为什么这样做有效?取地址运算符要求其“操作数必须是lvalue”。”并不是特别有用。 - Evan Teran
基本上它的意思是,“如果你可以取得它的地址,那么它就是一个左值,因为只有当你可以取得它的地址时,它才能成为一个左值。” - Evan Teran
它的用处在于大多数开发人员已经知道在哪里可以使用&运算符。如果它是一个定义,当然会有缺陷,但它确实是一个经验法则。 - Nemanja Trifunovic

2

lvalue中的“L”通常被描述为代表“位置”。lvalue指定了某物的位置;正如另一个回答者所指出的,lvalue通常可以取其地址。这就是为什么数字文字和非引用函数返回值不是lvalue的原因。

在const被引入C++之前,“L”曾经代表“left”。const lvalue不能出现在赋值语句的左边。


1

对于C编程语言,C11 draft n1570 6.3.2.1p1 简洁地表述:

一个lvalue是一个表达式(具有非void对象类型),它可能指定一个对象[...]

就是这么简单。

维基百科definition在回答写作时几乎同样好。

[...] 指向存储位置的值,可以潜在地允许分配新值或提供可访问的内存地址,其中数据可以被写入变量的位置值。


左值表达式的例子? 给定

int a, b[1], *c;

我们可以有以下左值表达式:abc,每个都明确地指代一个对象。b[0]也指代一个对象,因此它是一个左值表达式。那么潜在性呢?*c始终是一个左值表达式,但如果c不持有指向有效对象的指针,则不指代对象。b[1]在语义上可能指代一个对象,但由于访问了数组越界,所以实际上并没有。它们两个都是左值表达式。
当评估实际上并不指代对象的左值表达式时,行为是未定义的。
左值表达式可能比这更复杂,例如:
((flag ? foo() : bar())->pointed_to_by_struct_member[42])[0] 

如果它编译通过,那么它就是一个左值表达式。

基本上,在C中您可以应用 &(取地址运算符)的任何东西都是一个左值,除了函数,因为在C中函数不是对象。

那么L代表什么呢?

C11 n1570 Footnote 64

64) 名称lvalue最初来自赋值表达式E1 = E2,其中左操作数E1必须是(可修改的)左值。 它可能更好地被认为代表一个对象的定位器值。[...]


请注意,在ISO C中,E1 = E2不是必需的编译条件,以使E1成为lvalue。6.3.2.1p1继续说:

可修改的lvalue是指不具有数组类型、不具有不完整类型、不具有const限定类型,并且如果它是结构体或联合体,则不具有任何成员(包括递归地所有包含的聚合体或联合体的成员或元素)具有const限定类型。

在C之前(例如B语言),所有定位器值都可以出现在赋值的左侧,因此得名。这在C中不再适用,因为数组从未被分配,并且ISO C添加了const


P.S. 同样的注释解释了相关术语 rvalue,如下所示:

[...] 有时被称为rvalue的内容在国际标准中被描述为表达式的值。 [...]


0

一个绝对不是左值的简单示例:

3 = 42;

它确实提出了对“终极问题”的(令人费解的)解释。 - Rob

0
从这篇文章中。由于提问者有点懒(尽管有些人不同意,见评论),我也会懒一点,直接把整个相关部分粘贴在这里,可能会侵犯一些版权法。
对象是一块可以被检查和存储的存储区域。左值是一个指向此类对象的表达式。左值并不一定允许修改指定的对象。例如,常量对象是左值,但不能被修改。可修改左值这个术语用于强调左值允许指定对象被修改和检查。以下对象类型是左值,但不是可修改的左值:
- 数组类型 - 不完整类型 - 带const限定的类型 - 对象是结构或联合类型,其中一个成员具有带const限定的类型
由于这些左值是不可修改的,所以它们不能出现在赋值语句的左侧。
在C++中,返回引用的函数调用是左值。否则,函数调用是右值表达式。在C++中,每个表达式都会产生左值、右值或无值。
某些运算符需要对一些操作数使用左值。下表列出了这些运算符及其使用的附加约束条件。
     运算符                          要求
     & (一元)                         操作数必须是一个左值。
     ++ --                             操作数必须是一个左值。
                                          这适用于前缀和后缀形式。
     = += -= *= %= >= &= ^= |=         左操作数必须是一个左值。
例如,所有赋值运算符都会评估其右操作数,并将该值分配给其左操作数。左操作数必须是可修改的左值或可修改对象的引用。
地址运算符(&)要求一个左值作为操作数,而增量(++)和减量(--)运算符要求一个可修改的左值作为操作数。

这位楼主在提问之前甚至都没有提到他已经查阅了哪些资料。为什么不能建议他先去谷歌一下呢? - eljenso
因为http://stackoverflow.com/faq不要求、建议或描述在发布问题之前进行Google搜索。该网站的目的是成为第一线资源,而不是当Google失败时的备份。 - chaos
不要在这个问题上过于教条主义。楼主很懒,承认吧。展示你之前做过一些研究,并指明你不理解lvalues的地方将是有帮助的。除非你想把SO变成另一个维基百科?……什么是维基百科? - eljenso
那就是重点。常见问题解答并没有说你必须要除了懒惰之外的其他特质,因为你不需要。事实上,SO正在建立一个技术答案的维基数据库,而不仅仅是另一个技术帮助论坛。如果它是后者,那么当然可以GIYF,但它不是。 - chaos
好的,我们就各自持有不同意见吧。我甚至会删除谷歌这部分,因为人们可能会误解,尽管在他的情况下它仍然是更好的选择。 - eljenso
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接