为什么 c = ++(a+b) 会导致编译错误?

113

研究后,我发现增量运算符要求操作数具有可修改的数据对象:https://en.wikipedia.org/wiki/Increment_and_decrement_operators

由此推测,它会导致编译错误,因为(a+b)是一个临时整数,因此无法修改。

这个理解正确吗?这是我第一次尝试研究问题,如果我应该寻找什么,请指教。


35
就研究而言,这个还不错。你正在走上正确的道路。 - StoryTeller - Unslander Monica
35
你希望这个表达式能做什么? - qrdl
4
根据C11标准6.5.3.1规定:前缀递增或递减运算符的操作数必须是具有原子、限定或未限定实数或指针类型的可修改左值。 - Christian Gibbons
10
你希望将数字 1 分配给 a 和 b,应该从0还是1开始设置数组索引?我的妥协方案0.5未被采纳,我认为这并没有得到适当的考虑。——斯坦·凯利-布特尔 - Andrew Morton
5
我认为一个跟进的问题是,当c = a + b + 1更清晰且输入更短时,为什么您还想要这样做。增减运算符实现了两种操作:1.它们和它们的参数组成一个表达式(可以在for循环等中使用),2.它们修改参数。在您的示例中,您正在使用属性1,但不是属性2,因为您扔掉了修改后的参数。如果您不需要属性2,只想要表达式,则可以直接编写表达式,例如x+1,而不是x++。 - Trevor
显示剩余7条评论
8个回答

119

这只是一个规则而已,可能存在的原因有(1)使编写C编译器更容易,(2)没有人说服C标准委员会放宽此规定。

通俗来说,只有当foo可以出现在类似于foo = bar的赋值表达式的左边时,才能编写++foo。因为你不能编写a + b = bar,所以也不能编写++(a + b)

实际上并没有什么理由阻止a + b产生一个临时变量来执行++操作,其结果就是表达式++(a + b)的值。


4
我认为第一点说到了点子上。仅仅看一眼C++中临时材料化的规则就足以使人作呕(虽然它非常强大,必须得承认这一点)。 - StoryTeller - Unslander Monica
4
@StoryTeller:确实,与我们所喜爱的编程语言C++不同,C语言仍可以相对轻松地编译成汇编语言。 - Bathsheba
29
个人认为,这是一个真正的原因:如果 ++ 有时会具有修改某些内容的副作用,而有时又不会,那么将会非常令人困惑。 - aschepler
5
@dng:的确是这样;这就是为什么引入了lvalue和rvalue这些术语,尽管现在情况比那更加复杂(特别是在C++中)。例如,常量永远不能是lvalue:像是5 = a这样的表达式是没有意义的。 - Bathsheba
6
@Bathsheba 这就解释了为什么5++也会导致编译错误。 - dng
显示剩余7条评论

41
C11标准第6.5.3.1节规定,前缀递增或递减运算符的操作数应具有原子、限定、未限定的实数类型或指针类型,并且应为可修改的左值。"可修改的左值"在第6.3.2.1节的第1小节中描述。左值是一个表达式(具有除void以外的对象类型),它可能指代一个对象;如果左值在评估时不指代对象,则行为是未定义的。当说一个对象具有特定类型时,该类型由用于指定对象的左值指定。可修改左值是一个左值,它没有数组类型,没有不完整类型,没有const限定类型,并且如果它是结构体或联合体,则没有任何成员(包括递归地包含在所有包含聚合体或联合体的所有成员或元素中的任何成员)具有const限定类型。因此,(a+b)不是可修改的左值,因此不能使用前缀递增运算符。

1
你从这些定义中得出的结论是缺失的... 你想说的是 (a+b) 不可能指代一个对象,但这些段落并不支持这一点。 - hkBst

21

你说得对。 ++ 尝试将新值赋给原始变量,所以 ++a 将获取 a 的值,加上 1 并将其重新分配给 a。正如你所说,(a+b)是一个临时值,而不是具有分配的内存地址的变量,因此无法执行赋值操作。


12
我认为你已经回答了自己的问题。我可能会对你的措辞做出微小的改变,将“临时变量”替换为C.Gibbons提到的“rvalue”。
随着您学习C语言的内存模型(这看起来像一个不错的概述:https://www.geeksforgeeks.org/memory-layout-of-c-program/),变量、参数、临时变量等术语将变得更加清晰。
当您刚开始学习时,“rvalue”这个术语可能看起来很模糊,因此我希望以下内容有助于您对其产生一种直觉。 lvalue/rvalue是在等号(赋值运算符)两侧讨论的不同方面: lvalue = 左侧(小写L,不是“1”) rvalue = 右侧
学习一些关于C如何使用内存(和寄存器)的知识将有助于您了解为什么区分很重要。大致上,编译器创建了一个计算表达式结果(rvalue)的机器语言指令列表,然后将该结果(lvalue)“放置”到某个地方。 想象一下编译器处理以下代码片段:
x = y * 3

在汇编伪代码中,它可能看起来像这个玩具示例:

load register A with the value at memory address y
load register B with a value of 3
multiply register A and B, saving the result in A
write register A to memory address x

++运算符(以及其--对应项)需要“某个地方”进行修改,基本上任何可以作为左值的东西都可以。
理解C内存模型将是有帮助的,因为你会更好地了解参数如何传递给函数,以及(最终)如何使用动态内存分配,例如malloc()函数。出于类似的原因,您可能会在某个时候学习一些简单的汇编程序设计,以更好地了解编译器正在做什么。此外,如果您正在使用gcc,则 -S 选项“在编译阶段停止;不要装配。”可能很有趣(尽管我建议在代码片段上尝试它)。
只是一句话: ++指令自1969年以来就存在(尽管它始于C的前身B):

(Ken Thompson)的观察结果是++x的翻译比x = x + 1的翻译更小。

跟随维基百科的参考资料将带您进入Dennis Ritchie(“K&R C”中的“R”)有关C语言历史的有趣写作,这里为了方便而链接:http://www.bell-labs.com/usr/dmr/www/chist.html,您可以搜索“++”。

6
原因是标准要求操作数必须是左值。表达式 (a+b) 不是左值,因此不允许应用增量运算符。
现在,有人可能会说:“好吧,确实没有除此之外的*真正*原因”,但不幸的是,运算符的工作方式的特定措辞实际上确实要求如此。

表达式++E等同于(E+=1)。

显然,如果E不是左值,则无法编写E += 1。这很遗憾,因为可以说:“将E增加一”并完成。在这种情况下,在非左值上应用运算符(原则上)是完全可能的,但代价是使编译器稍微复杂一些。
现在,定义可以轻松地重新措辞(我认为它甚至不是最初的C语言,而是B的继承者),但这样做将从根本上改变语言,使其不再与早期版本兼容。由于可能的好处相对较小,但可能的影响非常大,因此从未发生过,并且可能永远不会发生。
如果您还考虑了C ++(问题标记为C,但有关运算符重载的讨论),则故事变得更加复杂。在C中,很难想象这可能是情况,但在C ++中,(a+b)的结果很可能是您根本无法增加的东西,或者增加可能具有非常重要的副作用(不仅仅是添加1)。编译器必须能够应对此,并在出现问题的情况下进行诊断。在左值上,这仍然相当简单。对于任何一种你向可怜的东西扔进括号内的杂乱表达式,情况就不是这样了。
这不是一个真正的原因,说明为什么不能这样做,但它确实作为解释,为什么实现这个功能的人们并不是特别狂热,以添加这种只对极少数人带来非常小好处的功能。

3

++试图将值赋给原始变量,由于(a+b)是临时值,无法执行操作。这些基本上是C编程约定的规则,以使编程更加简单。就是这样。


3

表达式(a+b)的值是一个右值,不能被递增。


2

当执行 ++(a+b) 表达式时,举个例子:

int a, b;
a = 10;
b = 20;
/* NOTE :
 //step 1: expression need to solve first to perform ++ operation over operand
   ++ ( exp );
// in your case 
   ++ ( 10 + 20 );
// step 2: result of that inc by one 
   ++ ( 30 );
// here, you're applying ++ operator over constant value and it's invalid use of ++ operator 
*/
++(a+b);

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