C++和 PHP 与 C# 和 Java - 结果不相等

2022-08-30 15:06:01

我在C#和Java中发现了一些奇怪的东西。让我们看一下这个C++代码:

#include <iostream>
using namespace std;

class Simple
{
public:
    static int f()
    {
        X = X + 10;
        return 1;
    }

    static int X;
};
int Simple::X = 0;

int main() {
    Simple::X += Simple::f();
    printf("X = %d", Simple::X);
    return 0;
}

在控制台中,您将看到X = 11(在此处查看结果 - IdeOne C++)。

现在,让我们看一下 C# 上的相同代码:

class Program
{
    static int x = 0;

    static int f()
    {
        x = x + 10;
        return 1;
    }

    public static void Main()
    {
        x += f();
        System.Console.WriteLine(x);
    }
}

在控制台中,您将看到 1(不是 11!(看看这里的结果 - IdeOne C#我知道你现在的想法 - “这怎么可能?”,但让我们转到下面的代码。

Java 代码:

import java.util.*;
import java.lang.*;
import java.io.*;

/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
    static int X = 0;
    static int f()
    {
        X = X + 10;
        return 1;
    }
    public static void main (String[] args) throws java.lang.Exception
    {
        Formatter f = new Formatter();
        f.format("X = %d", X += f());
        System.out.println(f.toString());
    }
}

结果与 C# 中的结果相同(X = 1,请在此处查看结果)。

最后一次,让我们看一下PHP代码:

<?php
class Simple
{
    public static $X = 0;

    public static function f()
    {
        self::$X = self::$X + 10;
        return 1;
    }
}

$simple = new Simple();
echo "X = " . $simple::$X += $simple::f();
?>

结果为 11(在此处查看结果)。

我有一点理论 - 这些语言(C#和Java)正在堆栈上制作静态变量X的本地副本(它们是否忽略了static关键字?)。这就是为什么这些语言的结果是1的原因。

有人在这里,谁有其他版本?


答案 1

C++标准规定:

对于不确定排序的函数调用,复合赋值的操作是单个评估。[ 注意:因此,函数调用不应干预左值到右值转换与任何单个复合赋值运算符相关的副作用。

§5.17 [expr.ass]

因此,就像您使用的同一评估和具有副作用的函数一样,结果是未定义的,因为:XX

如果相对于同一标量对象上的另一个副作用或使用同一标量对象的值计算,标量对象上的副作用未排序,则行为未定义。

§1.9 [介绍执行]

在许多编译器上,它恰好是11,但不能保证C++编译器不会像其他语言那样给你1。

如果您仍然持怀疑态度,那么对标准的另一次分析得出了同样的结论:该标准在上述同一部分中也说:

窗体的表达式的行为等效于,只是只计算一次。E1 op = E2E1 = E1 op E2E1

在你的情况下,除了只评估一次。
由于对求值顺序没有保证,因此在 中,您不能想当然地认为首先计算了 f,然后 。X = X + f()XX + f()X

补遗

我不是Java专家,但Java规则明确指定了表达式中的计算顺序,在Java语言规范的第15.7节中保证从左到右。在第 15.26.2 节中。复合赋值运算符 Java 规范也说这相当于 .E1 op= E2E1 = (T) ((E1) op (E2))

在 Java 程序中,这再次意味着您的表达式等效于 并首先计算,然后计算 。因此,在结果中没有考虑副作用。X = X + f()Xf()f()

所以你的Java编译器没有错误。它只是符合规格。


答案 2

感谢重复数据删除和user694733的评论,这是我原始答案的修改版本。


C++版本具有未定义的未指定行为。

“未定义”和“未指定”之间存在微妙的区别,因为前者允许程序执行任何操作(包括崩溃),而后者允许它从一组特定的允许行为中进行选择,而无需指示哪个选择是正确的。

除了非常罕见的情况外,您总是希望避免两者。


理解整个问题的一个很好的起点是C++常见问题解答 为什么有些人认为x = ++y + y ++不好?i ++ + i ++的价值是什么?以及“序列点”有什么问题?

在上一个序列点和下一个序列点之间,标量对象应通过表达式的求值最多修改一次其存储值。

(...)

基本上,在C和C++中,如果您在表达式中读取变量两次,并且还编写它,则结果是未定义的

(...)

在执行序列中称为序列点的某些指定点,先前评估的所有副作用应是完整的,并且不应发生后续评估的副作用。(...)称为序列点的“某些指定点”是(...)在计算函数的所有参数之后,但在执行函数中的第一个表达式之前。

简而言之,在两个连续的序列点之间修改变量两次会产生未定义的行为,但函数调用会引入一个中间序列点(实际上是两个中间序列点,因为 return 语句会创建另一个序列点)。

这意味着,表达式中有一个函数调用,这一事实“保存”了您的行,使其免于未定义,并将其转换为“仅”未指定。Simple::X += Simple::f();

1和11都是可能的和正确的结果,而打印123,崩溃或向老板发送侮辱性电子邮件是不允许的行为;您将永远无法保证打印1或11。


下面的示例略有不同。这似乎是对原始代码的简化,但实际上有助于突出未定义和未指定行为之间的区别:

#include <iostream>

int main() {
    int x = 0;
    x += (x += 10, 1);
    std::cout << x << "\n";
}

这里的行为确实是未定义的,因为函数调用已经消失,所以两个修改都发生在两个连续的序列点之间。C++语言规范允许编译器创建一个程序,该程序可以打印123,崩溃或向您的老板发送侮辱性电子邮件。x

(当然,电子邮件只是一种非常常见的幽默尝试,试图解释未定义实际上意味着任何事情。崩溃通常是未定义行为的更现实的结果。

实际上,(就像原始代码中的 return 语句一样)是一条红鲱鱼。以下情况也会产生未定义的行为:, 1

#include <iostream>

int main() {
    int x = 0;
    x += (x += 10);
    std::cout << x << "\n";
}

这可能会打印20(它在我的机器上使用VC ++ 2013这样做),但行为仍然未定义。

(注意:这适用于内置运算符。运算符重载将行为更改回指定,因为重载运算符从内置运算符复制语法,但具有函数的语义,这意味着出现在表达式中的自定义类型的重载运算符实际上是函数调用。因此,不仅引入了序列点,而且整个歧义消失了,表达式变得等价于 ,这保证了参数求值的顺序。这可能与您的问题无关,但无论如何都应该提到。+=x.operator+=(x.operator+=(10));

相比之下,Java版本

import java.io.*;

class Ideone
{
    public static void main(String[] args)
    {
        int x = 0;
        x += (x += 10);
        System.out.println(x);
    }
}

必须打印 10。这是因为Java在评估顺序方面既没有未定义也没有未指定的行为。没有要关注的序列点。请参阅 Java 语言规范 15.7。评估顺序

Java编程语言保证运算符的操作数看起来是按特定的评估顺序(即从左到右)计算的。

因此,在 Java 案例中,从左到右解释,意味着首先将某物添加到 0,而某物为 0 + 10。因此 0 + (0 + 10) = 10x += (x += 10)

另请参阅 Java 规范中的示例 15.7.1-2。

回到您的原始示例,这也意味着具有静态变量的更复杂的示例已在 Java 中定义并指定了行为。


老实说,我不了解C#和PHP,但我想它们都有一些有保证的评估顺序。C++,与大多数其他编程语言(但像C一样)不同,它倾向于允许比其他语言更多的未定义和未指定的行为。这可不是好事,也不是坏事。这是稳健性和效率之间的权衡。为特定任务或项目选择正确的编程语言始终是分析权衡的问题。

无论如何,具有这种副作用的表达式在所有四种语言中都是糟糕的编程风格

最后一句话:

我在C#和Java中发现了一个小错误。

如果您没有多年的软件工程师专业经验,则不应假设在语言规范编译器中发现错误。


推荐