1

我正在编写一些 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++ 编译器(Linux、Win 和 Mac 下的 g++、Borland、Code::blocks 和 MingW)测试它的输出是110, 100, 40. 我无法理解输出如何50, 80, 90......

为什么 MSVC 的输出不同?

4

4 回答 4

14

以下三个子表达式的求值顺序未指定:

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

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

这是因为在该表达式的任何地方都没有序列点。唯一的要求是在需要结果之前(即在打印结果之前)对每个子表达式求值。

在您的示例中,实际上有几个子表达式,我在这里将其标记为 a 到 k:

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

operator<<对( a, c, d, g, 和)的调用h都必须按顺序进行评估,因为每个调用都取决于前一次调用的结果。同样,b必须在可以评估之前a进行评估,并且k必须在 、 或 可以评估之前j进行i评估h

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

有关序列点和相关未指定和未定义行为的更多信息,请考虑阅读 Stack Overflow C++ 常见问题解答文章“未定义行为和序列点” (您的程序没有任何未定义行为,但文章的大部分内容仍然适用)。

于 2011-03-01T17:28:40.093 回答
3

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

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

于 2011-03-01T17:33:35.260 回答
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;
}

我同意其他人在他们的回答中所说的话,但这会让你确切地看到它在做什么。:)

于 2011-03-01T17:40:48.000 回答
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;

或者……继续结合。

于 2011-03-01T18:04:08.793 回答