这是一个测试用例:
void foo(int i, int j)
{
printf("%d %d", i, j);
}
...
test = 0;
foo(test++, test);
我希望得到“0 1”输出,但我得到“0 0”是什么给出的?
这是一个测试用例:
void foo(int i, int j)
{
printf("%d %d", i, j);
}
...
test = 0;
foo(test++, test);
我希望得到“0 1”输出,但我得到“0 0”是什么给出的?
这是未指定行为的示例。该标准没有说明应该以什么顺序评估参数。这是编译器实现的决定。编译器可以自由地以任何顺序评估函数的参数。
在这种情况下,看起来实际上是从右到左处理参数,而不是预期的从左到右。
一般来说,在参数中做副作用是不好的编程习惯。
而不是foo(test++, test); 你应该写foo(test, test+1); 测试++;
它在语义上等同于您要完成的任务。
编辑:正如安东尼正确指出的那样,在没有中间序列点的情况下读取和修改单个变量是未定义的。所以在这种情况下,行为确实是undefined。所以编译器可以自由地生成它想要的任何代码。
这不仅仅是未指定的行为,它实际上是未定义的行为。
是的,参数评估的顺序是未指定的,但是在没有中间序列点的情况下读取和修改单个变量是未定义的,除非读取只是为了计算新值。函数参数的评估之间没有序列点,因此f(test,test++)
未定义的行为:test
正在为一个参数读取并为另一个参数进行修改。如果您将修改移动到一个函数中,那么您就可以了:
int preincrement(int* p)
{
return ++(*p);
}
int test;
printf("%d %d\n",preincrement(&test),test);
这是因为在 entry 和 exit to 上有一个序列点preincrement
,因此必须在简单读取之前或之后评估调用。现在订单只是未指定。
另请注意,逗号运算符提供了一个序列点,因此
int dummy;
dummy=test++,test;
很好 --- 增量发生在读取之前,因此dummy
设置为新值。
原来我说的都是错的!计算副作用的时间点是未指定的。如果 test 是一个局部变量,Visual C++ 将在调用 foo() 之后执行递增,但如果 test 声明为静态或全局,它将在调用 foo() 之前递增并产生不同的结果,尽管最终值为测试将是正确的。
增量应该在调用 foo() 之后在单独的语句中完成。即使在 C/C++ 标准中指定了该行为,它也会令人困惑。您会认为 C++ 编译器会将其标记为潜在错误。
这是对序列点和未指定行为的一个很好的描述。
<----开始错错错---->
“test++”的“++”位在调用 foo 之后被执行。所以你将 (0,0) 传递给 foo,而不是 (1,0)
下面是 Visual Studio 2002 的汇编器输出:
mov ecx, DWORD PTR _i$[ebp]
push ecx
mov edx, DWORD PTR tv66[ebp]
push edx
call _foo
add esp, 8
mov eax, DWORD PTR _i$[ebp]
add eax, 1
mov DWORD PTR _i$[ebp], eax
增量是在调用 foo() 之后完成的。虽然这种行为是设计使然,但它肯定会让不经意的读者感到困惑,应该避免。增量应该在调用 foo() 之后在单独的语句中完成
<----END OF Wrong Wrong Wrong ---->
这是“未指定的行为”,但在实践中,通过指定 C 调用堆栈的方式,它几乎总是保证您将看到它为 0, 0 而绝不是 1, 0。
正如有人指出的那样,VC 的汇编器输出首先将最右边的参数压入堆栈。这就是在汇编器中实现 C 函数调用的方式。这是为了适应 C 的“无限参数列表”特性。通过以从右到左的顺序推送参数,第一个参数保证是堆栈的顶部项目。
取 printf 的签名:
int printf(const char *format, ...);
这些椭圆表示未知数量的参数。如果参数从左到右推送,则格式将位于我们不知道大小的堆栈的底部。
知道在 C(和 C++)中参数是从左到右处理的,我们可以确定解析和解释函数调用的最简单方法。到达参数列表的末尾,开始推送,随时评估任何复杂的语句。
然而,即使这样也不能拯救你,因为大多数 C 编译器都有解析函数“Pascal 风格”的选项。这意味着函数参数以从左到右的方式压入堆栈。例如,如果 printf 是用 Pascal 选项编译的,那么输出很可能是 1, 0 (但是,由于 printf 使用椭圆,我不认为它可以编译成 Pascal 风格)。
C 不保证函数调用中参数的评估顺序,因此您可能会得到结果“0 1”或“0 0”。编译器之间的顺序可以改变,同一个编译器可以根据优化参数选择不同的顺序。
写 foo(test, test + 1) 然后在下一行做 ++test 会更安全。无论如何,编译器应该尽可能优化它。
函数参数的求值顺序是未定义的。在这种情况下,它似乎是从右到左进行的。
(修改序列点之间的变量基本上允许编译器做它想做的任何事情。)
嗯,既然已经编辑了 OP 以保持一致性,它与答案不同步。关于评估顺序的基本答案是正确的。但是 foo(++test, test); 的具体可能值是不同的。案子。
++test将在通过之前递增,因此第一个参数将始终为 1。第二个参数将为 0 或 1,具体取决于评估顺序。
根据 C 标准,在单个序列点中有多个对变量的引用是未定义的行为(在这里您可以将其视为语句或函数的参数),其中多个引用中的一个包括前/后修改。所以: foo(f++,f) <--未定义 f 何时增加。同样(我一直在用户代码中看到这一点): *p = p++ + p;
通常,编译器不会更改其对此类事物的行为(主要修订除外)。
通过打开警告并注意它们来避免它。
重复其他人所说的话,这不是未指定的行为,而是未定义的行为。该程序可以合法地输出任何内容或不输出任何内容,将 n 保留为任何值,或向您的老板发送侮辱性电子邮件。
作为实践,编译器编写者通常只会做他们最容易编写的事情,这通常意味着程序将获取 n 一次或两次,调用函数,并在某个时间递增。这就像任何其他可以想象的行为一样,根据标准是很好的。没有理由期望编译器或版本之间或具有不同编译器选项的行为相同。没有理由为什么必须一致地编译同一程序中的两个不同但外观相似的示例,尽管我敢打赌。
简而言之,不要这样做。如果你很好奇,可以在不同的情况下进行测试,但不要假装只有一个正确甚至可以预测的结果。
编译器可能不会按照您期望的顺序评估参数。