我是一个介绍C ++类的TA。上周在测试中询问了以下问题:
以下程序的输出是什么:
int myFunc(int &x) {
int temp = x * x * x;
x += 1;
return temp;
}
int main() {
int x = 2;
cout << myFunc(x) << endl << myFunc(x) << endl << myFunc(x) << endl;
}
对我和我的所有同事来说,答案显然是:
8
27
64
但是现在有几个学生指出,当他们在某些环境中运行时,他们实际上是相反的:
64
27
8
当我使用gcc在我的linux环境中运行它时,我得到了我期望的结果。在我的Windows机器上使用MinGW,我得到了他们正在谈论的内容。 它似乎是先评估对myFunc的最后一次调用,然后是第二次调用然后是第一次调用,然后一旦它具有所有结果,它将以正常顺序输出它们,从第一次开始。但由于呼叫是按顺序进行的,因此数字相反。
在我看来,我是一个编译器优化,选择以相反的顺序评估函数调用,但我不知道为什么。我的问题是:我的假设是否正确?这是背景中发生的事吗?或者有什么完全不同的东西?另外,我真的不明白为什么向后评估函数然后评估输出前进会有好处。由于ostream的工作方式,输出必须是前进的,但似乎功能的评估也应该向前推进。
感谢您的帮助!
答案 0 :(得分:15)
C ++标准没有定义完整表达式的子表达式的计算顺序,除了引入顺序的某些运算符(逗号运算符,三元运算符,短路逻辑运算符),以及表达式的事实组成函数/运算符的参数/操作数都在函数/运算符本身之前进行求值。
海湾合作委员会没有义务向你(或我)解释为什么要这样做。它可能是一个性能优化,可能是因为编译器代码出现了一些更短更简单的方式,可能是因为其中一个mingw编码员个人讨厌你,并且想要确保如果你做出的假设不是'由标准保证,您的代码出错了。欢迎来到开放标准的世界: - )
编辑添加:litb在关于(未)定义的行为下面做了一点。该标准表示如果在表达式中多次修改变量,并且如果该表达式存在有效的评估顺序,以便多次修改变量而中间没有序列点,则表达式具有未定义的行为。这不适用于此,因为变量在对函数的调用中被修改,并且在任何函数调用的开始处都有一个序列点(即使编译器内联它)。但是,如果您手动内联代码:
std::cout << pow(x++,3) << endl << pow(x++,3) << endl << pow(x++,3) << endl;
然后那将是未定义的行为。在这段代码中,编译器有效地评估所有三个“x ++”子表达式,然后是三次调用pow,然后开始对operator<<
的各种调用。由于此顺序有效且没有将x的修改分开的序列点,因此结果完全未定义。在您的代码段中,仅指定了执行顺序。
答案 1 :(得分:10)
究竟为什么这有不明确的行为。
当我第一次看到这个例子时,我觉得行为是定义良好,因为这个表达式实际上是一组函数调用的简写。
考虑这个更基本的例子:
cout << f1() << f2();
这扩展为一系列函数调用,其中调用类型取决于运算符是成员还是非成员:
// Option 1: Both are members
cout.operator<<(f1 ()).operator<< (f2 ());
// Option 2: Both are non members
operator<< ( operator<<(cout, f1 ()), f2 () );
// Option 3: First is a member, second non-member
operator<< ( cout.operator<<(f1 ()), f2 () );
// Option 4: First is a non-member, second is a member
cout.operator<<(f1 ()).operator<< (f2 ());
在最低级别,这些将生成几乎相同的代码,因此我将仅提及从现在开始的第一个选项。
是标准中的保证,编译器必须在输入函数体之前评估每个函数调用的参数。在这种情况下,必须在cout.operator<<(f1())
之前评估operator<<(f2())
,因为调用其他运算符需要cout.operator<<(f1())
的结果。
未指定的行为启动,因为虽然必须对运算符的调用进行排序,但他们的参数没有这样的要求。因此,结果顺序可以是以下之一:
f2()
f1()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());
或者:
f1()
f2()
cout.operator<<(f1())
cout.operator<<(f1()).operator<<(f2());
或者最后:
f1()
cout.operator<<(f1())
f2()
cout.operator<<(f1()).operator<<(f2());
答案 2 :(得分:2)
未指定评估函数调用参数的顺序。简而言之,您不应该使用具有影响语句含义和结果的副作用的参数。
答案 3 :(得分:0)
是的,根据标准,功能参数的评估顺序是“未指定”。
因此,不同平台的产出不同
答案 4 :(得分:0)
正如已经说过的那样,你已经徘徊在未定义行为的闹鬼森林中。为了获得每次可以消除副作用的预期:
int myFunc(int &x) {
int temp = x * x * x;
return temp;
}
int main() {
int x = 2;
cout << myFunc(x) << endl << myFunc(x+1) << endl << myFunc(x+2) << endl;
//Note that you can't use the increment operator (++) here. It has
//side-effects so it will have the same problem
}
或将函数调用分解为单独的语句:
int myFunc(int &x) {
int temp = x * x * x;
x += 1;
return temp;
}
int main() {
int x = 2;
cout << myFunc(x) << endl;
cout << myFunc(x) << endl;
cout << myFunc(x) << endl;
}
第二个版本可能更适合测试,因为它迫使他们考虑副作用。
答案 5 :(得分:0)
这就是为什么每次你写一个有副作用的功能时,上帝会杀死一只小猫!