C 中的表达式求值 vs Java

Expression evaluation in C vs Java

int y=3;
int z=(--y) + (y=10);

在 C 语言中执行时 z 的值计算为 20 但是当 java 中的相同表达式执行时给出的 z 值为 12.

谁能解释为什么会这样,有什么区别?

when executed in C language the value of z evaluates to 20

不,它没有。这是未定义的行为,因此 z 可以获得任何值。包括 20。该程序理论上也可以做任何事情,因为标准没有说明程序在遇到未定义行为时应该做什么。在这里阅读更多:Undefined, unspecified and implementation-defined behavior

根据经验,切勿在同一表达式中修改变量两次。

这不是一个很好的副本,但这会更深入地解释事情。这里未定义行为的原因是序列点。 Why are these constructs using pre and post-increment undefined behavior?

在 C 中,当涉及到算术运算符时,如 +/,标准中未指定操作数的评估顺序,因此如果对这些操作数的评估有侧效果,你的程序变得不可预测。这是一个例子:

int foo(void)
{
    printf("foo()\n");
    return 0;
}

int bar(void)
{
    printf("bar()\n");
    return 0;
}

int main(void)
{
    int x = foo() + bar();
}

这个程序会打印什么?好吧,我们不知道。我不完全确定此代码段是否会调用 未定义的行为,但无论如何,输出是不可预测的。我提出了一个问题,Is it undefined behavior to use functions with side effects in an unspecified order?,所以我稍后会更新这个答案。

其他一些变量指定了计算顺序(从左到右),例如||&&,此功能用于短路。例如,如果我们使用上面的示例函数并使用 foo() && bar(),则只会执行 foo() 函数。

我不是很精通Java,但是为了完整起见,我想提一下Java除了非常特殊的情况外,基本上没有未定义或未指定的行为。 Java 中的几乎所有内容都定义明确。有关详细信息,请阅读

When executed in C language the value of z evaluates to 20

事实并非如此。您使用的编译器将其计算为 20。另一个可以完全不同的方式评估它:https://godbolt.org/z/GcPsKh

这种行为称为未定义行为。

你的表达有两个问题

  1. C 中未指定求值顺序(逻辑表达式除外)(这是一种未指定的行为)
  2. 在此表达式中,sequence point(未定义行为)也存在问题

这个答案分为 3 个部分:

  1. 这在 C 中是如何工作的(未指定的行为)
  2. 这在 Java 中是如何工作的(规范清楚地说明了应该如何评估)
  3. 为什么会有差异。

对于#1,您应该阅读@klutt 的精彩回答。

对于#2 和#3,您应该阅读此答案。

它在 java 中如何工作?

与 C 不同,java 的语言规范更为明确。例如,C 甚至没有告诉您数据类型 int 应该有多少位,而 java 语言规范却告诉您:32 位。即使在 64 位处理器和 64 位 java 实现上。

java 规范清楚地表明 x+y 将被评估 left-to-right (相对于 C 的 'in any order you please, compiler'),因此,首先评估 --y这显然是 2(使 y 为 2 的 side-effect),然后评估 y=10 显然是 10(具有使 y 10 的副作用),然后 2+10 是评价这明明是12.

显然,像 java 这样的语言更好;毕竟,未定义的行为在定义上几乎就是一个错误,C 语言规范编写者引入这些疯狂的东西有什么问题吗?

答案是:性能。

在 C 中,您的源代码由编译器转换为机器代码,然后由 CPU 解释机器代码。两步模型。

在java中,你的源代码被编译器转化为字节码,然后字节码被运行时转化为机器码,然后机器码被CPU解释。三步模型。

如果你想引入优化,你无法控制 CPU 的作用,所以对于 C 来说,只有 1 个步骤可以完成:编译。

所以 C(语言)旨在为 C 编译器提供大量自由,以尝试生成优化的机器代码。这是一个 cost/benefit 场景:以在 lang 规范中包含大量 'undefined behaviour' 为代价,您可以获得更好的优化编译器的好处。

在 java 中,您进行了第二步,这就是 java 进行优化的地方:在运行时。 java.exe 对 class 个文件执行此操作; javac.exe 相当 'stupid' 并且几乎没有优化。这是故意的;在运行时你可以做得更好(例如,你可以使用一些簿记来跟踪两个分支中的哪一个更常被采用,因此分支预测比 C 应用程序更好) - 这也意味着 cost/benefit 分析现在结果是:语言规范应该一目了然。

所以 java 代码永远不会是未定义的行为?

不是这样。 Java 有一个内存模型,其中包含大量未定义的行为:

class X { int a, b; }
X instance = new X();

new Thread() { public void run() {
    int a = instance.a;
    int b = instance.b;
    instance.a = 5;
    instance.b = 6;
    System.out.print(a);
    System.out.print(b);
}}.start();

new Thread() { public void run() {
    int a = instance.a;
    int b = instance.b;
    instance.a = 1;
    instance.b = 2;
    System.out.print(a);
    System.out.print(b);
}}.start();

在 java 中未定义。它可能会打印 005600120010000256000600 以及更多可能性。很难想象 5000(它可以合法打印)之类的东西:如何读取 a 'work' 但读取 b 然后失败?

出于完全相同的原因,您的 C 代码会产生任意答案:

优化。

规范中 'hardcoding' 的 cost/benefit 正是这段代码的行为方式会带来很大的成本:您会带走大部分优化空间。所以 java 付出了代价,现在有一个 langspec 是模棱两可的,只要你 modify/read 来自不同线程的相同字段没有建立 so-called 'comes-before' 守卫使用例如synchronized.