为什么不同的C ++编译器会为此代码提供不同的结果?

时间:2011-03-01 17:24:38

标签: c++ visual-c++ expression-evaluation unspecified-behavior

我正在编写一些C ++代码,用于娱乐和练习,以了解有关语言功能的更多信息。我想更多地了解静态变量及其在递归函数中的行为。在g ++编译器中尝试此代码,我得到了预期的结果:

#include <iostream>
using namespace std;

int f(const int& value)
{
   static int result = 0;
   return result += value;
}

int main()
{
   cout << f(10) << ", " << f(f(10)) << ", " << f(f(f(10)));
   return 0;
}

但是我的朋友在Microsoft Visual C ++ 6中测试了相同的代码。输出是50, 80, 90我用其他C ++编译器测试它(g ++,Borland,Code :: blocks和Linux下的MingW,Win和Mac)输出是{ {1}}。我无法理解输出可能是110, 100, 40 ...

为什么MSVC的输出不同?

4 个答案:

答案 0 :(得分:14)

以下三个子表达式的评估顺序未指定:

f(10)
f(f(10))
f(f(f(10)))

编译器可以按任何顺序评估这些子表达式。您不应该依赖程序中特定的评估顺序,特别是如果您打算使用多个编译器进行编译。

这是因为该表达式中没有任何序列点。唯一的要求是在需要结果之前(即,在打印结果之前)评估每个子表达式。

在你的例子中,实际上有几个子表达式,我在这里标记为通过:

//   a  b     c       d  e f      g       h  i j k
cout << f(10) << ", " << f(f(10)) << ", " << f(f(f(10)));

operator<<acdgh)的调用都必须按顺序进行评估因为每个都取决于前一个电话的结果。同样,必须先评估b,然后才能评估a,并且必须在kji之前评估h可以评估。

但是,其中一些子表达式之间没有依赖关系:b的结果不依赖于k的结果,因此编译器可以自由生成评估k的代码然后bb然后k

有关序列点以及相关的未指定和未定义行为的更多信息,请考虑阅读Stack Overflow C ++ FAQ文章"Undefined Behavior and Sequence Points"(您的程序没有任何未定义的行为,但本文的大部分内容仍然适用)。

答案 1 :(得分:3)

仅仅因为输出在屏幕上从左向右显示并不意味着评估顺序遵循相同的方向。在C ++中,函数参数的评估顺序是未指定。另外,通过<<运算符打印数据只是用于调用函数的精确语法。

简而言之,如果你说operator<<(foo(), bar()),编译器可以先调用foobar。这就是为什么调用具有副作用的函数并将其用作其他函数的参数通常是个坏主意。

答案 2 :(得分:2)

一种简单的方法来确切了解它在做什么:

int f(const int& value, int fID)
{
   static int result = 0;
   static int fCounter = 0;
   fCounter++;
   cout << fCounter << ".  ID:" << fID << endl;    
   return result += value;
}

int main()
{
   cout << f(10, 6) << ", " << f(f(10, 4), 5) << ", " << f(f(f(10, 1),2),3);
   return 0;
}

我同意其他人在答案中所说的话,但这可以让你看到它到底在做什么。 :)

答案 3 :(得分:2)

前缀运算符语法转换为以下前缀表示法:

<<( <<( <<( cout, f(10) ), f(f(10)) ), f(f(f(10))) )
 A   B   C

现在有三种不同的函数调用,上面标识为A,B和C.每个调用的参数是:

     arg1        arg2
A: result of B, f(10)
B: result of C, f(f(10))
C: cout       , f(f(f(10)))

对于每个调用,允许编译器以任何顺序评估参数,为了正确评估A的第一个参数,必须首先评估B,对于B的第一个参数,类似地,必须评估整个C表达式。这意味着第一个参数依赖项所需的A,B和C的执行存在部分顺序。对每个调用和两个参数的评估也有部分排序,所以B 1 和B 2 (指的是调用B的第一个和第二个参数)在B之前进行评估。

这些部分排序不会导致执行调用的唯一要求,因为编译器可以在尝试计算第一个参数之前决定执行所有第二个参数,从而导致等效路径:

tmp1 = f(10); tmp2 = f(f(10)); tmp3 = f(f(f(10)));
cout << tmp1 << tmp2 << tmp3;

tmp3 = f(f(f(10))); tmp2 = f(f(10)); tmp1 = f(10);
cout << tmp1 << tmp2 << tmp3;

tmp2 = f(f(10)); tmp1 = f(10); tmp3 = f(f(f(10)));
cout << tmp1 << tmp2 << tmp3;

或......继续合并。