繁体   English   中英

C 与 Java 中的表达式计算

[英]Expression evaluation in C vs Java

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

在 C 语言中执行时, z值为 20,但在 java 中执行相同的表达式时, z值为 12。

谁能解释为什么会发生这种情况以及有什么区别?

在 C 语言中执行时,z 的值为 20

不,不是的。 这是未定义的行为,因此z可以获得任何值。 包括20。程序理论上也可以做任何事情,因为标准没有说明程序遇到未定义行为时应该做什么。 在此处阅读更多信息:未定义、未指定和实现定义的行为

根据经验,不要在同一个表达式中两次修改变量。

这不是一个好的副本,但这会更深入地解释事情。 此处未定义行为的原因是序列点。 为什么这些构造使用前后增量未定义行为?

在 C 中,当涉及算术运算符时,例如+/ ,标准中未指定操作数的求值顺序,因此如果对这些求值有副作用,您的程序将变得不可预测。 下面是一个例子:

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

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

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

这个程序会打印什么? 好吧,我们不知道。 我不完全确定这个片段是否会调用未定义的行为,但无论如何,输出是不可预测的。 我提出了一个问题,以未指定的顺序使用具有副作用的函数是否是未定义的行为? ,关于那个,所以我稍后会更新这个答案。

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

我对Java不是很精通,但为了完整性,我想提一下,除了非常特殊的情况,Java基本上没有未定义或未指定的行为。 Java 中的几乎所有内容都定义良好。 有关更多详细信息,请阅读rzwitserloot 的回答

这个答案有 3 个部分:

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

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

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

它在java中是如何工作的?

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

Java 规范清楚地表明x+y是从左到右求值的(相对于 C 的“按你喜欢的任何顺序,编译器”),因此,首先--y被求值,这显然是 2(侧面- y 2 的效果),然后评估y=10显然是 10(带有使 y 10 的副作用),然后评估2+10显然是 12。

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

答案是:性能。

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

在java中,你的源代码被编译器转换成字节码,字节码然后被运行时转换成机器码,然后机器码被CPU解释。 一个 3 步模型。

如果要引入优化,则无法控制 CPU 做什么,因此对于 C,可以完成的步骤只有 1 个:编译。

因此,C(语言)旨在为 C 编译器提供大量自由,以尝试生成优化的机器代码。 这是一个成本/收益方案:以在 lang 规范中有大量“未定义行为”为代价,您可以获得更好的优化编译器的好处。

在 Java 中,您有第二步,这就是 Java 进行优化的地方:在运行时。 java.exe对类文件进行处理; javac.exe非常“愚蠢”,几乎没有优化。 这是故意的; 在运行时,您可以做得更好(例如,您可以使用一些簿记来跟踪两个分支中的哪一个更常被采用,从而分支预测比 C 应用程序更好) - 这也意味着成本/收益分析现在产生in:lang 规范应该是清晰的。

所以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 代码会产生任意答案:

优化。

规范中“硬编码”的成本/收益正是此代码的行为方式,这将带来很大的成本:您将占用大部分优化空间。 因此,java 付出了代价,现在有了一个 langspec,每当您修改/读取来自不同线程的相同字段时,它都是不明确的,而无需使用例如synchronized建立所谓的“先来”保护。

在 C 语言中执行时,z 的值为 20

这不是事实。 您使用的编译器将其计算为20 另一个可以以完全不同的方式对其进行评估: https : //godbolt.org/z/GcPsKh

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

你的表达有两个问题。

  1. C 中未指定评估顺序(逻辑表达式除外)(这是一种未指定的行为)
  2. 在这个表达式中,序列点(Undefined Bahaviour)也有问题

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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