[英]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,您应该阅读@klutt 的精彩回答。
对于#2 和#3,您应该阅读此答案。
与 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 有一个内存模型,其中包含大量未定义的行为:
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 代码会产生任意答案:
优化。
规范中“硬编码”的成本/收益正是此代码的行为方式,这将带来很大的成本:您将占用大部分优化空间。 因此,java 付出了代价,现在有了一个 langspec,每当您修改/读取来自不同线程的相同字段时,它都是不明确的,而无需使用例如synchronized
建立所谓的“先来”保护。
在 C 语言中执行时,z 的值为 20
这不是事实。 您使用的编译器将其计算为20
。 另一个可以以完全不同的方式对其进行评估: https : //godbolt.org/z/GcPsKh
这种行为称为未定义行为。
你的表达有两个问题。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.