我有这个Code,我在Java和C上运行它,但它们给了我两个不同的结果.是什么让他们以不同的方式运行.
x=10;y=10;z=10; y-=x--; z-=--x; x-=--x-x--;
Java中X的输出值为:8,对于C,输出为6.
这两个编译器对增量选项的行为有何不同?
当你说这段代码的输出被认为是C程序时,你错了6
.
被视为C程序,这是未定义的.你恰好用你的编译器得到6,但你也可能得到24,分段错误或编译时错误.
参见C99标准 6.5.2:
在前一个和下一个序列点之间,一个对象的存储值最多只能通过表达式的评估来修改一次.此外,先前的值应该是只读的,以确定要存储的值.71)
--x-x--
本段明确禁止.
编辑:
Aaron Digulla在评论中写道:
它真的未定义吗?
您是否注意到我链接到C99标准并指出段落说明这是未定义的?
gcc -Wall(GCC 4.1.2)没有抱怨这个,我怀疑任何编译器都会拒绝这个代码.
该标准将某些行为描述为"未定义",因为并非所有C程序无意义的方法都可以在编译时可靠地检测到.如果你认为"没有警告"应该意味着一切都很好,你应该转用另一种语言而不是C.许多现代语言都有更好的定义.当我有一个选择时,我使用OCaml,但是有无数其他明确定义的语言.
有一个原因,它返回6,你应该能够解释它.
我没有注意到你为什么这个表达式被评估为6的解释.我希望你不要花太多时间写它,因为对我来说它返回0.
Macbook:~ pascalcuoq$ cat t.c #includeint 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;
.您是否声称此构造是有效的C并且优秀的工程师应该能够预测结果,因为没有编译器警告它?这个结构是未定义的,就像正在讨论的那个.
最后,如果您绝对需要警告以确定存在问题,请考虑使用Frama-C等验证工具.它需要做一些不在标准中的假设来捕获一些现有的实践,但是它正确地警告了--x-x--
大多数其他未定义的C行为.
该术语如何评估?--x - x--
对于Java和C ,右侧都评估为0,但它会更改x
。所以问题是:如何-=
工作?它x
是在评估右侧(RHS)之前读取读数,然后减去RHS还是在评估RHS之后进行读数。那你有
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。
x-x
是0
x的任何值。
--x-x
是0
对于x的任何值,因为它可以被写为--x, x-x
x-x--
这是0
因为负运算符的结果是在递减后计算的。
因此,如果前减量对结果没有影响,而后减量也没有影响。而且,两个运算符之间没有推断(以与in相同的表达式使用它们a = --y - x--
不会改变其行为)。结论:所有和任何C编译器将返回0
的--x - x--
(当然,除了那些越野车)。
这使我们有了我最初的假设:RHS 值对结果没有影响,它总是求值0
但会修改 x
。那么问题是如何-=
实施?有很多因素在这里起作用:
CPU是否具有本机运算符-=
?基于寄存器的CPU可以(实际上,它们只有这样的运算符。要做的是a+b
,它们必须复制a
到寄存器中,然后才能复制到寄存器中+=b
),基于堆栈的CPU则不需要(它们将所有值压入堆栈,然后使用使用最高堆栈元素作为操作数的运算符)。
值是保存在堆栈还是寄存器中?(问第一个问题的另一种方法)
哪些优化选项处于活动状态?
要进一步讲,我们必须看一下代码:
#includeint 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
。如您所见,编译器实际上注意到了这一点x-x
并对其进行了优化0
(即使没有任何优化选项)。我们还有两个预期的--
操作,这意味着结果必须始终为6
。
那么谁是对的?我们俩都是。当Pascal说标准没有定义这种行为时,他是对的。但这并不意味着它是随机的。代码的所有部分都有明确定义的行为,因此总和的行为不能突然变得不确定(除非有其他遗漏,但在这种情况下则不能如此)。因此,即使标准不解决此问题,它仍然是确定性的。
对于基于堆栈的CPU(没有任何寄存器),结果应为8,因为它们将x
在开始评估右侧之前复制的值。对于基于寄存器的CPU,它应该始终为6。
士气:标准始终是正确的,但是如果您必须理解,请查看代码;)