我有这段代码,我在Java和C上运行它,但它们给了我两个不同的结果。是什么让它们运行不同?
x=10;y=10;z=10;
y-=x--;
z-=--x;
x-=--x-x--;
Java中X的值为8,C的值为6。
这两个编译器在增量选项方面的行为有何不同?
我有这段代码,我在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标准并指出了认为这是未定义的段落吗?
标准之所以将某些行为描述为“未定义”,恰恰是因为不是所有 C 程序无意义的方式都可以在编译时可靠地检测到。如果您认为“没有警告”就意味着一切正常,那么您应该换用其他语言而不是 C 语言。许多现代语言的定义更好。我有选择时会使用 OCaml,但还有无数其他定义良好的语言。gcc -Wall(GCC 4.1.2)没有抱怨这个问题,我怀疑任何编译器都不会拒绝这段代码。
它返回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
这是你认为我的编译器有 bug 的时间(因为它没有返回与你的相同的东西)。
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)
Aaron也写道:
作为一名工程师,你仍然应该能够解释为什么会返回一个结果或另一个结果。
没错!我给出了最简单的解释,为什么可能会得到6:在C99中,结果被明确指定为未定义行为,并且在早期标准中也是如此。
还有:
最后,请展示一个可以警告这个结构的编译器。
据我所知,没有编译器会对*(&x - 1)
发出警告,其中x
由int x;
定义。你是在说这个结构是有效的C语言,并且一个好的工程师应该能够预测结果,因为没有编译器对其发出警告吗?这个结构是未定义的,就像正在讨论的那个一样。
最后,如果您绝对需要警告以相信存在问题,请考虑使用Frama-C等验证工具。它需要做一些不在标准中的假设来捕获一些现有的实践,但它正确地警告--x-x--
和大多数其他未定义的C行为。
gcc -Wall
(GCC 4.1.2)不会对此发出警告,我怀疑任何编译器都不会拒绝这段代码。 - Aaron Digulla--x - x--
尝试在序列点之间多次修改对象,因此行为是未定义的。 - John Bode这个术语是如何评估的?在Java和C中,右边的表达式--x - x--
都会计算为0,但它会改变x
。所以问题是: -=
是如何工作的?它是在计算右侧(RHS)之前读取x
,然后减去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
解析此结构时,您将获得此解析树:
+---- (-=) ----+
v -= v
x +--- (-) ----+
v v
PREDEC x POSTDEC x
x
进行三次修改(一次在左侧,两次在两个递减操作中),将使 x
变为未定义。好的。但编译器是一个确定性程序,因此当它接受某些输入时,它总是会产生相同的输出。而且大多数编译器都是一样的。我认为我们都同意任何 C 编译器实际上都会接受这个输入。我们可以期望什么输出呢?答案是:6 或 8。原因如下:
x-x
在任何 x
的取值下都为 0
。--x-x
在任何 x
的取值下都为 0
,因为它可以被写成 --x, x-x
。x-x--
的结果为 0
,因为减号运算符的结果在后缀递减之前计算。a = --y - x--
不会改变它们的行为)。结论:所有的C编译器都将返回0
,对于--x - x--
(除了有缺陷的编译器)。
这使我们得出了我最初的假设:右侧值value不会影响结果,它总是计算为0
,但它会修改x
。那么问题是-=
如何实现?这里有很多因素起作用:
-=
运算符?基于寄存器的CPU有(实际上,它们只有这样的运算符。要执行a+b
,它们必须将a
复制到一个寄存器中,然后才能对其进行+=b
),而基于堆栈的CPU则没有(它们将所有值都推入堆栈,然后使用将最上面的堆栈元素作为操作数的运算符)。要进一步了解,我们必须查看代码:
#include <stdio.h>
int main() {
int x = 8;
x -= --x - x--;
printf("x=%d\n", x);
}
.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
。x
的值。对于基于寄存器的CPU,它应该始终为6。嗯…你认为哪一个是正确的,你的理由是什么?
我认为第一到三步的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
在没有正式描述操作符在这种情况下的行为的情况下,谁能说哪个是正确的?
x -= --x - x--
的所有可能方式都是完全正确的,根据C标准,包括你的任何一个例子,返回42或格式化硬盘。这是未定义的行为。 - David Thornley在C语言中,语句
x -= --x - x--;
该表达式没有内部序列点。它只有在开头和结尾处的一个序列点。这意味着无法确定该表达式语句的评估顺序。就C语言时间而言,它是不可分割的,如上所述。每当有人试图通过强加特定的时间顺序来解释此处发生的事情时,他们只是浪费时间并产生彻底的无意义。这实际上是C语言不会(也不能)尝试定义具有相同对象的多个修改的表达式行为的原因(如上例中的x
)。该行为未定义。
Java在这方面显然有很大不同。在Java中,“时间”的概念被定义得不同。在Java中,表达式总是按照运算符优先级和结合性定义的严格顺序进行评估。这对于在评估上述表达式期间发生的事件强制施加了严格的时间顺序。这使得该表达式的结果被定义,与C不同。