Java和C编译器对代码的行为有何不同?

7

我有这段代码,我在Java和C上运行它,但它们给了我两个不同的结果。是什么让它们运行不同?

x=10;y=10;z=10;
y-=x--;
z-=--x;
x-=--x-x--;

Java中X的值为8,C的值为6

这两个编译器在增量选项方面的行为有何不同?


2
请各位解释一下 -1s 是什么意思。 - Ruben Bartelink
3
@Ruben: 因为编写这样的代码本身就是没有意义的,只适合在学校考试中使用(甚至在那里都可争议)。如果他只是出于好奇,那还好…不管怎样,如果我看到我的同事编写这样的代码,我会对他的编码质量有非常不好的印象。 - RageZ
9
因为它们是两种不同的语言,有着不同的规则。 - Mehrdad Afshari
1
@RageZ:好吧,但为什么不说清楚呢(毕竟问问题的人只是想更好地理解吧?) - Ruben Bartelink
同意 - 清晰比简洁更重要,除非简洁能带来清晰。我们公司的规定是始终使用前缀递增和前缀递减,因为根据我们的经验,它们不太容易被误解,并且在表达式中使用时通常更快。这有时会导致像"a=x; ++x"这样的代码,可以缩短为"a=x++;", 但我们认为标准化值得在这里多加一行。 - Dathan
6个回答

29

当你说这段代码作为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
#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)发出警告,其中xint x;定义。你是在说这个结构是有效的C语言,并且一个好的工程师应该能够预测结果,因为没有编译器对其发出警告吗?这个结构是未定义的,就像正在讨论的那个一样。

最后,如果您绝对需要警告以相信存在问题,请考虑使用Frama-C等验证工具。它需要做一些不在标准中的假设来捕获一些现有的实践,但它正确地警告--x-x--和大多数其他未定义的C行为。


这真的是未定义的吗?我还没有看过C99,但在ANSI C中,没有规定为什么这应该是非法的。我手头没有太多的C编译器,但gcc -Wall(GCC 4.1.2)不会对此发出警告,我怀疑任何编译器都不会拒绝这段代码。 - Aaron Digulla
@Aaron:是的,它确实是未定义的。如果你认为Pascal引用标准有误,但自己没有查看过,那么就没有其他办法可以说服你了。他引用的句子也出现在C89中的“3.3表达式”(不过我只是在看草案的镜像,没有C89标准的副本)。 - Steve Jessop
@Aaron:illegal 不等于 undefined。同时,附录 J.2(未定义行为)中还有这样一种语言:“在两个序列点之间,对象被修改超过一次,或者被修改并且之前的值被读取,而不是为了确定要存储的值(6.5)。” 表达式 --x - x-- 尝试在序列点之间多次修改对象,因此行为是未定义的。 - John Bode
@Aaron: "未定义"意味着标准对实现的行为没有任何限制,因此它可以做任何事情(拒绝编译、执行可能合理的操作、给你岳母发极客笑话),只要符合规范。显然,gcc 4.1.2试图做一些可能是你所想的事情,这是完全合法的,尽管有个警告会更好。其他编译器可能会以不同方式猜测你的意思,或者拒绝猜测,这也是完全合法的。 - David Thornley
@Aaron:C和C++编译器可能会编译这样的代码,但不能保证。你可能会得到一个结果。确实有一个原因,为什么在一个特定版本的编译器的一个特定试验中返回6,并且你可以通过研究该特定编译器如何解析表达式来解释它。没有特别的理由相信它会一致地返回6,或者另一个编译器会返回6。试图建立未定义行为的期望是愚蠢和徒劳的。 - David Thornley
显示剩余6条评论

4

这个术语是如何评估的?在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

在Java中,这是规则:
复合赋值表达式E1 op = E2等价于E1 = (T)((E1) op (E2)),其中T是E1的类型,但E1只被评估一次。(参见15.26.2 Compound Assignment Operators)
这意味着该值被复制,因此前缀和后缀递减没有效果。您的C编译器可能使用不同的规则。
对于C语言,this article 可能有所帮助:
道德是,在任何语言中编写依赖于评估顺序的代码都是不良的编程实践。
[编辑] Pascal Cuoq(见下文)坚称标准规定结果是未定义的。这可能是正确的:我花了几分钟时间看他从标准中复制出来的部分,也无法理解那个句子的含义。我想我在这里不是唯一一个困惑的人 :) 所以我去看了一下我为我的硕士论文开发的C解释器是如何工作的。它不符合标准,但我知道它的工作原理。猜猜,我是一种海森堡式的人:我可以在任何精度下都有一个,但不能同时拥有两个 ;) 无论如何。

解析此结构时,您将获得此解析树:

        +---- (-=) ----+
        v     -=       v
        x        +--- (-) ----+
                 v            v
              PREDEC x    POSTDEC x

标准规定,对 x 进行三次修改(一次在左侧,两次在两个递减操作中),将使 x 变为未定义。好的。但编译器是一个确定性程序,因此当它接受某些输入时,它总是会产生相同的输出。而且大多数编译器都是一样的。我认为我们都同意任何 C 编译器实际上都会接受这个输入。我们可以期望什么输出呢?答案是:6 或 8。原因如下:
  1. x-x 在任何 x 的取值下都为 0
  2. --x-x 在任何 x 的取值下都为 0,因为它可以被写成 --x, x-x
  3. x-x-- 的结果为 0,因为减号运算符的结果在后缀递减之前计算。
因此,如果前缀递减对结果没有影响,后缀递减也没有影响。此外,两个运算符之间没有推断(在同一表达式中使用它们,如 a = --y - x-- 不会改变它们的行为)。结论:所有的C编译器都将返回0,对于--x - x--(除了有缺陷的编译器)。

这使我们得出了我最初的假设:右侧值value不会影响结果,它总是计算为0,但它会修改x。那么问题是-=如何实现?这里有很多因素起作用:

  1. CPU是否有本地的-=运算符?基于寄存器的CPU有(实际上,它们只有这样的运算符。要执行a+b,它们必须将a复制到一个寄存器中,然后才能对其进行+=b),而基于堆栈的CPU则没有(它们将所有值都推入堆栈,然后使用将最上面的堆栈元素作为操作数的运算符)。
  2. 值是保存在堆栈还是寄存器中的?(另一种问法第一个问题)
  3. 哪些优化选项是启用的?

要进一步了解,我们必须查看代码:

#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

第一个 movlx设置为8,这意味着-4(%rbp)x。正如您所看到的,编译器实际上注意到了x-x并将其优化为0,就像预测的那样(即使没有任何优化选项)。我们还有两个预期的--操作,这意味着结果必须始终为6
那么谁是对的?我们都是对的。当Pascal说标准没有定义此行为时,他是正确的。但这并不意味着它是随机的。代码的所有部分都具有明确定义的行为,因此总和的行为不能突然未定义(除非还有其他缺少的内容-但在这种情况下没有)。因此,即使标准没有处理此问题,它仍然是确定性的。
对于基于堆栈的CPU(没有任何寄存器),结果应该是8,因为它们将在开始评估右侧之前复制x的值。对于基于寄存器的CPU,它应该始终为6。
教训:标准总是正确的,但如果您必须理解,请查看代码;)

6
该程序不符合C语言标准,其行为在语言中是未定义的。 - Stephen Canon
1
在标准没有禁止的情况下,这是合法的。但结果没有保障。如果编译器文档没有提到它,那么编译器可能会产生任何结果,并且这可能会有所不同。编译器实现者通常不关心未定义行为的处理方式,因此一般来说,编译器将执行对于类似事物方便定义的操作。请注意,算术运算符(或其他很多东西)的求值顺序从未得到保证,并且可以在同一编译器中有所不同(例如,为了辅助常见子表达式的求值)。 - David Thornley
1
无论编译器是否接受,它都不是有效的C语言。引用标准(6.5,第2段):“在前一个和下一个序列点之间,一个对象通过表达式的评估最多只能修改其存储值一次。” - Stephen Canon
3
@Aaron:我希望很容易找到一个 C 编译器,在某些情况下,返回不同的结果。我甚至预计这可能会因为优化级别的变化而有所不同。 - erikkallen
2
@aaron 完成了:我的编译器给这个表达式赋值为0。详见我的回答。 - Pascal Cuoq
显示剩余11条评论

4
在C++中,结果是不确定的,即未指定或保证一致 - 编译器随时可以根据序列点做出最适合它的任何操作。 我怀疑Java [和C#等]也是如此。

这里哪里说了结果是不确定的?按照我的理解,运算符的结果应该始终为6。 - Thorarin
前/后缀递减的情况显然不是简单的L值。 - Ruben Bartelink
@Thorarin:什么说明前置和后置增量是从左到右(或者反之)处理的?请参阅序列点文章。 - Ruben Bartelink
啊,好好读了一下。我之前对这个的影响没有意识到。具体来说,除了顺序点的概念之外,C++标准还规定,如果访问一个经过修改的表达式的先前值以外的任何目的,那么该表达式的行为是未定义的。所以,array[x++]的行为是未定义的 - 尽管在我使用过的大多数编译器中,它们通常表现得很友好,将后缀递增的评估推迟到赋值结束的顺序点。 - Dathan
与 C 不同,Java 中定义了这个顺序。 - Blindy
显示剩余3条评论

3

嗯…你认为哪一个是正确的,你的理由是什么?

我认为第一到三步的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

在没有正式描述操作符在这种情况下的行为的情况下,谁能说哪个是正确的?


2
在C语言中,处理x -= --x - x--的所有可能方式都是完全正确的,根据C标准,包括你的任何一个例子,返回42或格式化硬盘。这是未定义的行为。 - David Thornley

3
Java和C语言之间的根本区别在于,在C语言中,不同操作之间的时间关系(发生“之前”和发生“之后”)由所谓的“序列点”确定。序列点实现了C程序执行过程中“时间”的概念。如果两个动作之间有一个序列点分隔开,则可以说一个动作发生在另一个动作之前或之后。当两个动作之间没有序列点时,它们之间没有定义的时间顺序,并且无法确定哪个动作先发生、哪个动作后发生。将C程序中的一对相邻序列点视为“最小不可分割时间单元”。在该时间单元内发生的事情不能用“之前”和“之后”来描述。你可以认为在两个相邻序列点之间,所有事情都在同时发生,或以任意顺序发生,取决于你的喜好。

在C语言中,语句

x -= --x - x--;

该表达式没有内部序列点。它只有在开头和结尾处的一个序列点。这意味着无法确定该表达式语句的评估顺序。就C语言时间而言,它是不可分割的,如上所述。每当有人试图通过强加特定的时间顺序来解释此处发生的事情时,他们只是浪费时间并产生彻底的无意义。这实际上是C语言不会(也不能)尝试定义具有相同对象的多个修改的表达式行为的原因(如上例中的x)。该行为未定义。

Java在这方面显然有很大不同。在Java中,“时间”的概念被定义得不同。在Java中,表达式总是按照运算符优先级和结合性定义的严格顺序进行评估。这对于在评估上述表达式期间发生的事件强制施加了严格的时间顺序。这使得该表达式的结果被定义,与C不同。


这是绝对正确的,但使用“时间”一词有些误导性。在序列点之前的表达式实际上不需要在时间上早于后面的表达式进行评估;相反,代码的可观察行为需要与如果严格遵守这样的时间顺序时的行为无法区分。这是一个重要的细节,因为编译器通常会在优化过程中跨序列点重新排序指令。 - Stephen Canon
是的,但我认为这是不同抽象级别的问题。我所描述的“时间”是“抽象C++机器”的时间,只要语言规范定义了严格的顺序,它就会按照严格的顺序评估所有内容。C++标准描述了该“抽象机器”的行为,而实际实现只需要模拟该机器的可观察行为即可。 - AnT stands with Russia
我只是希望这一点能够明确,而在你的帖子中似乎并不清楚。 - Stephen Canon

0
我不确定,但我猜想这是因为Java在计算最后一个x--时会先执行后减操作,然后执行-=运算符,而C ++则先执行-=,然后在整个表达式完成后执行后减操作。

2
在C++中,顺序不能保证在编译器之间(甚至在同一文件中的同一编译器内部)是一致的。在Java中更可能被指定,但不确定。 - Ruben Bartelink
@Rubin:根据标准,表达式的任何内容在C或C++中都无法保证一致性,因为它是未定义行为。至于Java,我不确定。 - David Thornley

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