C vs Java中的表达式评估

问题描述

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

使用C语言执行时,z的值等于20 但是如果在Java中使用相同的表达式,则在执行时给出的z值为12。

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

解决方法

使用C语言执行时,z的值为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几乎所有内容都定义良好。有关更多详细信息,请阅读rzwitserloot's answer

,

此答案分为3部分:

  1. 这在C语言中的工作方式(未指定的行为)
  2. 这在Java中是如何工作的(规范对此有明确的评估)
  3. 为什么有区别。

对于#1,您应该阅读@klutt的绝妙答案。

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

它如何在Java中工作?

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

Java规范清楚地指出,x+y是从左到右进行求值的(与C的“按您喜欢的任何顺序,编译器”相对),因此,首先对--y进行求值,即显然2(具有y 2的副作用),然后评估y=10显然是10(具有y 10的副作用),然后评估2+10显然是10 12。

显然,像Java这样的语言会更好。毕竟,从定义上说,未定义的行为几乎是一个错误,C语言规范编写者在介绍这种疯狂的东西时有什么毛病?

答案是:性能。

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

在Java中,编译器将源代码转换为字节码,然后在运行时将字节码转换为机器码,然后由CPU解释机器码。三步模型。

如果要引入优化,则无法控制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'工作'但不能读取b呢?

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

优化。

规范中“硬编码”的确切成本/收益确切地体现了该代码的行为方式,将为此付出巨大的代价:您将花费大部分的优化空间。因此,java支付了费用,并且现在有了langspec变得很模糊,只要您修改/读取来自不同线程的相同字段而没有使用例如synchronized

,

使用C语言执行时,z的值为20

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

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

在表达中,您有两个问题。

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

相关问答

依赖报错 idea导入项目后依赖报错,解决方案:https://blog....
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下...
错误1:gradle项目控制台输出为乱码 # 解决方案:https://bl...
错误还原:在查询的过程中,传入的workType为0时,该条件不起...
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct...