[英]How does the code behave different for Java and C compiler?
我有这段代码,我在Java和C上运行过,但是它们给了我两个不同的结果。 是什么使它们的运行方式不同。
x=10;y=10;z=10;
y-=x--;
z-=--x;
x-=--x-x--;
X的Java输出为: 8 , C的输出为6 。
这两个编译器对递增选项的行为有何不同?
当您说此代码的输出被视为C程序时,输出是6
。
被视为C程序, 未定义 。 您刚巧用编译器得到6,但是您也可能得到24,分段错误或编译时错误。
参见C99标准 6.5.2:
在上一个序列点与下一个序列点之间,对象的存储值最多只能通过对表达式的求值来修改。 此外,先验值应只读以确定要存储的值(71)
--xx--
被本段明确禁止。
编辑:
亚伦·迪古拉(Aaron Digulla)在评论中写道:
真的不确定吗?
您是否注意到我链接到C99标准,并指出了一段未定义的段落?
gcc -Wall(GCC 4.1.2)对此没有抱怨,我怀疑任何编译器都会拒绝该代码。
该标准将某些行为描述为“未定义”,恰恰是因为并非在编译时就可以可靠地检测到C语言的所有废话。 如果您认为“无警告”应表示一切正常,则应改用C语言以外的其他语言。许多现代语言的定义都更好。 我可以选择使用OCaml ,但还有无数其他定义明确的语言。
它有一个返回6的原因,您应该能够解释它。
我没有注意到您对此表达式为何求值为6的解释。我希望您不要花太多时间编写它,因为对我来说它返回0。
Macbook:~ pascalcuoq$ cat t.c
#include <stdio.h>
int main(int argc, char **argv)
{
int y;
printf("argc:%d\n", argc);
y = --argc - argc--;
printf("y:%d\n", y);
return 0;
}
Macbook:~ pascalcuoq$ gcc t.c
Macbook:~ pascalcuoq$ ./a.out 1 2 3 4 5 6 7 8 9
argc:10
y:0
这是您认为我的编译器中存在错误的时间(因为它未返回与您的错误相同的东西)。
Macbook:~ pascalcuoq$ gcc -v
Using built-in specs.
Target: i686-apple-darwin9
Configured with: /var/tmp/gcc/gcc-5490~1/src/configure --disable-checking -enable-werror --prefix=/usr --mandir=/share/man --enable-languages=c,objc,c++,obj-c++ --program-transform-name=/^[cg][^.-]*$/s/$/-4.0/ --with-gxx-include-dir=/include/c++/4.0.0 --with-slibdir=/usr/lib --build=i686-apple-darwin9 --with-arch=apple --with-tune=generic --host=i686-apple-darwin9 --target=i686-apple-darwin9
Thread model: posix
gcc version 4.0.1 (Apple Inc. build 5490)
亚伦还写道:
作为工程师,您仍然应该能够解释为什么它返回一个结果或另一个结果。
究竟! 我给出了一个最简单的解释,为什么会得到6:结果在C99中被明确指定为未定义行为,并且在早期标准中也是如此。
和:
最后,请显示警告此构造的编译器。
据我所知,没有编译器会警告*(&x - 1)
,其中x
是由int x;
定义的int x;
。 您是否声称此构造是有效的C,并且由于没有编译器警告该结果,所以好的工程师应该能够预测结果? 就像正在讨论的那样,这种构造是不确定的。
最后,如果您绝对需要警告以认为存在问题,请考虑使用验证工具,例如Frama-C 。 它需要做出一些不在标准中的假设来捕获一些现有的实践,但是它会正确警告--xx--
和大多数其他未定义的C行为。
该术语如何评估? 右侧--x - x--
对于Java和C都求值为0,但它会更改x
。 所以问题是: -=
如何工作? 它是在评估右侧(RHS)之前读取x
,然后减去RHS还是在评估RHS之后执行x
。 那你有
tmp = x // copy the value of x
x = tmp - (--x - x--) // complicated way to say x = x
要么
tmp = (--x - x--) // first evaluate RHS, from left to right, which means x -= 2.
x = x - tmp // substract 0 from x
在Java中,这是规则:
形式为E1 op = E2的复合赋值表达式等效于E1 =(T)((E1)op(E2)),其中T是E1的类型,只是E1仅被评估一次。 (请参阅15.26.2复合分配运算符 )
这意味着将复制的值,因此前减量和后减量均无效。 您的C编译器可能使用其他规则。
对于C, 本文可能会有所帮助:
道理是,编写依赖于评估顺序的代码对于任何语言而言都是不良的编程习惯。
[编辑] Pascal Cuoq(见下文)坚持认为标准说结果不确定。 这可能是正确的:我盯着他复制出来的那部分超出标准几分钟,却听不懂那句话的意思。 我想我并不孤单:)因此,我去看了为我的硕士论文开发的C解释器如何工作。 它不符合标准,但我知道它是如何工作的。 猜猜,我是一个海森堡式的人:我可以任意精确地选择一个,但不能同时选择两者;)无论如何。
解析此构造时,将获得以下解析树:
+---- (-=) ----+
v -= v
x +--- (-) ----+
v v
PREDEC x POSTDEC x
该标准指出,将x
修改3次(一次在左侧,两次在两个递减操作中两次),则x
仍未定义。 好的。 但是编译器是确定性程序,因此当它接受某些输入时,它将始终产生相同的输出。 而且大多数编译器的工作原理相同。 我认为我们都同意,任何C编译器实际上都会接受此输入。 我们可以期待什么输出? 答案:6或8。
xx
均为0
。 --xx
对于x的任何值都是0
,因为它可以写成--x, xx
xx--
为0
因为负运算符的结果是在递减后计算的。 因此,如果前减量对结果没有影响,而后减量也没有影响。 另外,两个运算符之间没有推断(在与a = --y - x--
相同的表达式中使用它们都不会改变其行为)。 结论:所有C编译器都会为--x - x--
返回0
(好吧,有错误的编译器除外)。
这让我们有了最初的假设:RHS 值对结果没有影响,它始终为0
但会修改 x
。 那么问题是-=
如何实现? 有很多因素在这里起作用:
-=
的本机运算符? 基于寄存器的CPU可以(实际上,他们只有这样的运算符。要执行a+b
,他们必须将a
复制到寄存器中,然后可以+=b
到它),基于堆栈的CPU则没有(它们将所有值,然后使用将最上面的堆栈元素用作操作数的运算符)。 要进一步讲,我们必须看一下代码:
#include <stdio.h>
int main() {
int x = 8;
x -= --x - x--;
printf("x=%d\n", x);
}
编译后,我们得到分配的汇编代码(x86代码):
.loc 1 4 0
movl $8, -4(%rbp) ; x = 8
.loc 1 5 0
subl $1, -4(%rbp) ; x--
movl $0, %eax ; tmp = 0
subl %eax, -4(%rbp) ; x -= tmp
subl $1, -4(%rbp) ; x--
.loc 1 6 0
movl -4(%rbp), %esi ; push `x` into the place where printf() expects it
第一movl
套x
至8
该装置-4(%rbp)
是x
。 如您所见,编译器实际上会注意到xx
并按预期将其优化为0
(即使没有任何优化选项)。 我们还有两个期望值--
运算,这意味着结果必须始终为6
。
那么谁是对的? 我们俩都是。 当Pascal说标准没有定义这种行为时,他是对的。 但这并不意味着它是随机的。 代码的所有部分都具有明确定义的行为,因此总和的行为不能突然变得不确定(除非缺少其他内容,但在这种情况下则不能如此)。 因此,即使标准不解决此问题,它仍然是确定性的。
对于基于堆栈的CPU(没有任何寄存器),结果应为8,因为它们将在开始评估右侧之前复制x
的值。 对于基于寄存器的CPU,它应该始终为6。
士气:标准始终是正确的,但是如果您必须理解,请查看代码;)
在C ++中,结果是不确定的,即未指定或保证结果是一致的-编译器可以随时根据序列点自由地执行最适合的操作。
我怀疑Java(和C#等)也是如此
好吧...您认为哪个是正确的,您的理由是什么?
我相信x
在前三个步骤中都非常确定
x = 10
x is decremented (its initial value is used first)
x is decremented again (its resulting value is used after)
现在x == 8
。 但是,请在此处查看您的操作(请避免插入人类友好的空白):
x -= --x - x--
可以将其编译为(如果我必须在我的语言中包括++
和--
运算符,这将是我要做的事情–首先确定副作用,然后将其从整个语句的开头和后方删除):
--x
t = x - x
x -= t
x--
给出x == 8
的结果。 也许它已经被编译为(该语句首先通过子表达式被还原):
t1 = --x // t1 = 7, x = 7
t2 = x-- // t2 = 7, x = 6
t = t1 - t2 // t = 7 - 7 = 0
x -= t // x = 6
或子表达式可能反过来出现:
t1 = x-- // t1 = 8, x = 7
t2 = --x // t2 = 6, x = 6
t = t2 - t1 // t = 6 - 8 = -2
x -= t // x = 8
在这种情况下,如果没有对操作员行为的正式描述,谁说这是正确的?
Java和C之间的根本区别在于,在C语言中,不同动作(“发生在”之前”和“发生在”之后)之间的时间关系由所谓的序列点确定。 顺序点在C程序的执行过程中实现了时间的概念。 如果两个动作被顺序点分开,那么您可以说一个动作发生在“之前”,而另一个动作发生在“之后”。 当两个动作之间没有序列点时,它们之间就没有定义的时间顺序,也就无法说出“先发生”和“后发生”的情况。 将C程序中的一对相邻序列点视为时间的最小不可分单位 。 在该时间单位内发生的情况无法用“之前”和“之后”来描述。 人们可能还认为,在两个相邻的序列点之间,所有事情都同时发生。 或以随机顺序排列,以您喜欢的为准。
用C语言声明
x -= --x - x--;
里面没有序列点。 它仅在开始和结束时都有一个序列点。 这意味着无法说出该表达式语句的评估顺序。 如上所述,它与C时间是不可分割的。 每当有人试图通过施加特定的时间顺序来解释这里发生的事情时,他们就是在浪费时间并产生完全的胡说八道。 这实际上是C语言不(也不能)试图对同一对象进行多次修改(上例中为x
)来定义表达式行为的原因。 该行为是不确定的。
Java在这方面有很大的不同。 在Java中, 时间的概念定义不同。 在Java中,表达式始终按运算符优先级和关联性定义的严格顺序求值。 这对上述表达式的评估过程中发生的事件施加了严格的时间顺序。 与C相反,这使该表达式的结果得以定义。
我不确定,但是我猜这是因为Java 在评估-=运算符之前先评估最后一个x--的后递减,而C ++首先评估-=,然后在整个表达式的其余部分评估后递减完成。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.