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
这种行为称为未定义行为。
你的表达有两个问题
- C 中未指定求值顺序(逻辑表达式除外)(这是一种未指定的行为)
- 在此表达式中,sequence point(未定义行为)也存在问题
这个答案分为 3 个部分:
- 这在 C 中是如何工作的(未指定的行为)
- 这在 Java 中是如何工作的(规范清楚地说明了应该如何评估)
- 为什么会有差异。
对于#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 中未定义。它可能会打印 0056
、0012
、0010
、0002
、5600
、0600
以及更多可能性。很难想象 5000
(它可以合法打印)之类的东西:如何读取 a
'work' 但读取 b
然后失败?
出于完全相同的原因,您的 C 代码会产生任意答案:
优化。
规范中 'hardcoding' 的 cost/benefit 正是这段代码的行为方式会带来很大的成本:您会带走大部分优化空间。所以 java 付出了代价,现在有一个 langspec 是模棱两可的,只要你 modify/read 来自不同线程的相同字段没有建立 so-called 'comes-before' 守卫使用例如synchronized
.
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
这种行为称为未定义行为。
你的表达有两个问题
- C 中未指定求值顺序(逻辑表达式除外)(这是一种未指定的行为)
- 在此表达式中,sequence point(未定义行为)也存在问题
这个答案分为 3 个部分:
- 这在 C 中是如何工作的(未指定的行为)
- 这在 Java 中是如何工作的(规范清楚地说明了应该如何评估)
- 为什么会有差异。
对于#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 中未定义。它可能会打印 0056
、0012
、0010
、0002
、5600
、0600
以及更多可能性。很难想象 5000
(它可以合法打印)之类的东西:如何读取 a
'work' 但读取 b
然后失败?
出于完全相同的原因,您的 C 代码会产生任意答案:
优化。
规范中 'hardcoding' 的 cost/benefit 正是这段代码的行为方式会带来很大的成本:您会带走大部分优化空间。所以 java 付出了代价,现在有一个 langspec 是模棱两可的,只要你 modify/read 来自不同线程的相同字段没有建立 so-called 'comes-before' 守卫使用例如synchronized
.