繁体   English   中英

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

[英]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输出为: 8C的输出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。

  1. 对于任何x值, xx均为0
  2. --xx对于x的任何值都是0 ,因为它可以写成--x, xx
  3. xx--0因为负运算符的结果是在递减后计算的。

因此,如果前减量对结果没有影响,而后减量也没有影响。 另外,两个运算符之间没有推断(在与a = --y - x--相同的表达式中使用它们都不会改变其行为)。 结论:所有C编译器都会为--x - x--返回0 (好吧,有错误的编译器除外)。

这让我们有了最初的假设:RHS 对结果没有影响,它始终为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

第一movlx8该装置-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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM