后增量运算符行为
可能重复:
C,C ++,Java和C#中的前后增量运算符行为
这是一个测试用例:
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); 试验++;
它在语义上等同于您要完成的任务。
编辑:正如安东尼正确指出的那样,在没有插入序列点的情况下读取和修改单个变量是未定义的。 所以在这种情况下,行为确实是未定义的 。 因此编译器可以自由地生成它想要的任何代码。
这不仅仅是未指定的行为,它实际上是未定义的行为 。
是的,参数评估的顺序是未指定的 ,但是在没有插入序列点的情况下读取和修改单个变量是未定义的 ,除非读取仅用于计算新值。 函数参数的评估之间没有序列点,因此f(test,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
设置为新值。
我原先说的一切都是错的! 计算副作用的时间点未指定。 如果test是局部变量,Visual C ++将在调用foo()之后执行增量,但如果test被声明为static或global,它将在调用foo()之前递增并产生不同的结果,尽管最终值为测试是正确的。
在调用foo()之后,增量应该在单独的语句中完成。 即使在C / C ++标准中指定了行为,也会令人困惑。 您会认为C ++编译器会将此标记为潜在错误。
以下是序列点和未指定行为的良好描述。
<----错误的错误开始---->
在调用foo之后执行“test ++”的“++”位。 所以你传入(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()之后,增量应该在单独的语句中完成
<----错误的错误结束---->
它是“未指定的行为”,但在实践中,通过指定C调用堆栈的方式,它几乎总能保证您将其视为0,0,而不是1,0。
正如有人指出的那样,VC的汇编器输出首先推送堆栈上最右边的参数。 这是在汇编程序中实现C函数调用的方式。 这是为了适应C的“无限参数列表”function。 通过按从右到左的顺序推送参数,第一个参数保证是堆栈中的顶级项目。
以printf的签名为准:
int printf(const char *format, ...);
这些椭圆表示未知数量的参数。 如果从左到右推送参数,则格式将位于堆栈的底部,我们不知道其大小。
知道在C(和C ++)中从左到右处理参数,我们可以确定解析和解释函数调用的最简单方法。 到达参数列表的末尾,并开始推送,评估任何复杂的语句。
但是,即使这样也无法保存,因为大多数C编译器都可以选择解析函数“Pascal样式”。 所有这些意味着函数参数以从左到右的方式被压入堆栈。 例如,如果使用Pascal选项编译printf,那么输出很可能是1,0(但是,因为printf使用椭圆,我认为它不能编译为Pascal样式)。
C不保证函数调用中参数的评估顺序,因此可能会得到结果“0 1”或“0 0”。 订单可以从编译器更改为编译器,同一编译器可以根据优化参数选择不同的订单。
编写foo(test,test + 1)然后在下一行中进行++测试会更安全。 无论如何,编译器应尽可能优化它。
编译器可能没有按照您期望的顺序评估参数。
函数参数的求值顺序是未定义的。 在这种情况下,它似乎是从右到左。
(修改序列点之间的变量基本上允许编译器执行它想要的任何操作。)
嗯,既然OP已被编辑以保持一致性,那么它与答案不同步。 关于评价顺序的基本答案是正确的。 但是foo的特定可能值是不同的(++ test,test); 案件。
++ test 将在传递之前递增,因此第一个参数将始终为1.第二个参数将为0或1,具体取决于评估顺序。
根据C标准,在单个序列点中对变量进行多次引用是不确定的行为(在这里您可以将其视为语句或函数的参数),其中一个或多个引用包括修改前/后。 所以:foo(f ++,f)< - 未定义何时f递增。 同样(我在用户代码中一直看到这一点):* p = p ++ + p;
通常,编译器不会更改此类事物的行为(主要修订除外)。
通过打开警告并注意它们来避免它。
重复别人所说的话,这不是未指明的行为,而是未定义的行为。 该程序可以合法地输出任何内容或任何内容,保留任何值,或向您的老板发送侮辱性电子邮件。
实际上,编译器编写者通常只会为他们编写最容易编写的东西,这通常意味着程序将获取n次或两次,调用函数,并在某个时间递增。 这与任何其他可想到的行为一样,根据标准是好的。 没有理由期望编译器,版本或不同的编译器选项之间存在相同的行为。 没有理由为什么必须一致地编译同一程序中的两个不同但看起来相似的例子,尽管这是我打赌的方式。
简而言之,不要这样做。 如果你很好奇,可以在不同情况下进行测试,但不要假装有一个正确甚至可预测的结果。