后增量运算符行为

时间:2008-09-19 00:13:26

标签: c++ c compiler-construction post-increment

  

可能重复:
  Pre & post increment operator behavior in C, C++, Java, & C#

这是一个测试用例:


void foo(int i, int j)
{
   printf("%d %d", i, j);
}
...
test = 0;
foo(test++, test);

我希望获得“0 1”输出,但我得到“0 0” 是什么给了??

10 个答案:

答案 0 :(得分:47)

这是未指定行为的示例。标准说明应该评估什么顺序参数。这是编译器实现决策。编译器可以按任何顺序自由地计算函数的参数。

在这种情况下,看起来实际上是从右到左处理参数,而不是从左到右处理。

一般来说,在参数中做副作用是糟糕的编程习惯。

而不是 foo(test ++,test); 你应该写 foo(test,test + 1);试验++;

它在语义上等同于你想要完成的任务。

编辑: 正如安东尼正确指出的那样,在没有插入序列点的情况下读取和修改单个变量是不明确的。所以在这种情况下,行为确实是未定义。因此编译器可以自由生成它想要的任何代码。

答案 1 :(得分:29)

这不仅仅是未指定的行为,实际上是未定义的行为

是的,参数评估的顺序是未指定,但它是未定义以读取和修改单个变量而没有插入序列点,除非读取仅用于计算新价值的目的。函数参数的评估之间没有序列点,因此f(test,test++)未定义的行为:正在为一个参数读取test并为另一个参数进行修改。如果你将修改移动到一个函数中,那么你没问题:

int preincrement(int* p)
{
    return ++(*p);
}

int test;
printf("%d %d\n",preincrement(&test),test);

这是因为在preincrement的进入和退出时有一个序列点,因此必须在简单读取之前或之后评估调用。现在订单只是未指定

另请注意,逗号运算符提供了一个序列点,所以

int dummy;
dummy=test++,test;

很好---增量在读取之前发生,因此dummy被设置为新值。

答案 2 :(得分:14)

我原先说的一切都是错的!计算副作用的时间点未指定。如果test是局部变量,Visual C ++将在调用foo()之后执行增量,但如果test被声明为static或global,它将在调用foo()之前递增并产生不同的结果,尽管最终值为测试是正确的。

在调用foo()之后,增量应该在单独的语句中完成。即使在C / C ++标准中指定了行为,也会令人困惑。你会认为C ++编译器会将此标记为潜在错误。

Here是对序列点和未指定行为的良好描述。

< ----错误的错误开始---->

“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()

之后,增量应该在单独的语句中完成

< ----错误错误的结束---->

答案 3 :(得分:6)

这是“未指定的行为”,但在实践中,通过指定C调用堆栈的方式,它几乎总能保证您将其视为0,0而不是1,0。

正如有人指出的那样,VC的汇编器输出首先推送堆栈上最右边的参数。这是在汇编程序中实现C函数调用的方式。这是为了适应C的“无限参数列表”功能。通过按从右到左的顺序推送参数,第一个参数保证是堆栈中的顶级项目。

获取printf的签名:

int printf(const char *format, ...);

这些椭圆表示未知数量的参数。如果从左到右推送参数,则格式将位于堆栈的底部,我们不知道其大小。

知道在C(和C ++)中从左到右处理参数,我们可以确定解析和解释函数调用的最简单方法。到达参数列表的末尾,并开始推送,评估任何复杂的语句。

但是,即使这样也无法保存,因为大多数C编译器都有解析函数“Pascal样式”的选项。所有这些意味着函数参数以从左到右的方式被压入堆栈。例如,如果使用Pascal选项编译printf,那么输出很可能是1,0(但是,因为printf使用椭圆,我不认为它可以编译为Pascal样式)。

答案 4 :(得分:2)

C不保证函数调用中参数的评估顺序,因此可能会得到结果“0 1”或“0 0”。订单可以从编译器更改为编译器,同一个编译器可以根据优化参数选择不同的订单。

写foo(测试,测试+ 1)然后在下一行进行++测试更安全。无论如何,编译器应尽可能优化它。

答案 5 :(得分:1)

编译器可能没有按照您期望的顺序评估参数。

答案 6 :(得分:1)

函数参数的求值顺序未定义。在这种情况下,它似乎从右到左。

(修改序列点之间的变量基本上允许编译器做任何想做的事。)

答案 7 :(得分:1)

嗯,既然OP已被编辑以保持一致性,那么它与答案不同步。关于评估顺序的基本答案是正确的。但是foo的特定可能值是不同的(++ test,test);情况下。

++ test 将在传递之前递增,因此第一个参数将始终为1.第二个参数将为0或1,具体取决于评估顺序。

答案 8 :(得分:1)

根据C标准,在单个序列点中对一个变量有多个引用是不确定的行为(这里你可以把它看作是一个语句,或者函数的参数),其中一个或多个这些参考包括前/后修改。 所以: foo(f ++,f)< - 未定义何时f递增。 同样(我在用户代码中一直看到这一点): * p = p ++ + p;

通常,编译器不会更改此类事物的行为(主要修订除外)。

通过打开警告并注意它们来避免它。

答案 9 :(得分:1)

重复别人所说的话,这不是未指明的行为,而是未定义的行为。该程序可以合法地输出任何内容或任何内容,保留任何值,或向您的老板发送侮辱性电子邮件。

作为一种惯例,编译器编写者通常只会为他们编写最容易编写的东西,这通常意味着程序将获取n次或两次,调用函数,并在某个时间递增。这与任何其他可想象的行为一样,根据标准是好的。没有理由期望编译器,版本或不同的编译器选项之间存在相同的行为。没有理由为什么同一个程序中的两个不同但相似的例子必须一致地编译,尽管这是我打赌的方式。

简而言之,不要这样做。如果你很好奇,可以在不同情况下进行测试,但不要假装有一个正确甚至可预测的结果。