这是未指定行为的示例.该标准没有说明应该评估哪些订单参数.这是一个编译器实现决策.编译器可以按任何顺序自由地计算函数的参数.
在这种情况下,它看起来实际上是从右到左处理参数而不是从左到右的预期处理.
一般来说,在参数中做副作用是糟糕的编程习惯.
而不是foo(test ++,test); 你应该写foo(test,test + 1); 试验++;
它在语义上等同于您要完成的任务.
编辑:正如安东尼正确指出的那样,在没有插入序列点的情况下读取和修改单个变量是未定义的.所以在这种情况下,行为确实是未定义的.因此编译器可以自由地生成它想要的任何代码.
这是未指定行为的示例.该标准没有说明应该评估哪些订单参数.这是一个编译器实现决策.编译器可以按任何顺序自由地计算函数的参数.
在这种情况下,它看起来实际上是从右到左处理参数而不是从左到右的预期处理.
一般来说,在参数中做副作用是糟糕的编程习惯.
而不是foo(test ++,test); 你应该写foo(test,test + 1); 试验++;
它在语义上等同于您要完成的任务.
编辑:正如安东尼正确指出的那样,在没有插入序列点的情况下读取和修改单个变量是未定义的.所以在这种情况下,行为确实是未定义的.因此编译器可以自由地生成它想要的任何代码.
这不仅仅是未指定的行为,它实际上是未定义的行为.
是的,参数评估的顺序是未指定的,但是在没有插入序列点的情况下读取和修改单个变量是未定义的,除非读取仅用于计算新值.函数参数的评估之间没有序列点,因此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
设置为新值.
我原先说的一切都是错的!在该侧影响被计算的时间点是不确定的.如果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的"无限参数列表"功能.通过按从右到左的顺序推送参数,第一个参数保证是堆栈中的顶部项.
以printf的签名:
int printf(const char *format, ...);
这些椭圆表示未知数量的参数.如果从左到右推送参数,则格式将位于堆栈的底部,我们不知道其大小.
知道在C(和C++)中从左到右处理参数,我们可以确定解析和解释函数调用的最简单方法.到达参数列表的末尾,并开始推送,评估任何复杂的语句.
但是,即使这样也无法保存,因为大多数C编译器都可以选择解析函数"Pascal样式".所有这些意味着函数参数以从左到右的方式被压入堆栈.例如,如果使用Pascal选项编译printf,那么输出很可能是1,0(但是,因为printf使用椭圆,我认为它不能编译为Pascal样式).