2008-09-19 103 views
15

可能重复:
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” 什么给?

+0

也许你的描述/例子应该完全包含标题问题? – 2008-09-19 00:18:52

+0

标题和代码示例不匹配 – itj 2008-09-19 08:02:24

+0

问题在标题和代码示例之间混淆。 title has ++ n例如test ++ – itj 2008-09-19 08:05:16

回答

47

这是未指定行为的示例。标准确定而不是说明应评估哪些顺序参数。这是一个编译器实现决策。编译器可以按任意顺序自由评估函数的参数。

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

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

代替FOO(试验++,测试);你应该写foo(test,test + 1);试验++;

这将是语义上等同于你正在试图完成什么。

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

+0

作为额外的重点,为了避免这些问题,我总是将增量作为单独的语句。 – 2008-09-19 01:17:18

1

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

14

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

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

Here是序列点,不确定的行为一个很好的说明。

< ----错错错START ---->

的 “++” “++试验” 的位在呼叫之后被执行到foo。所以,你在(0,0)传递给富,不是(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()

< ----错错就错---->

1

评价为参数的顺序给函数的一端未定义。在这种情况下,它似乎是从右到左做的。

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

2

C没有保证的参数计算顺序函数调用,所以这个你可能会得到的结果“ 0 1“或”0 0“。顺序可以从编译器更改为编译器,同一编译器可以根据优化参数选择不同的顺序。

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

6

这是“不确定的行为”,但在与C调用栈指定它几乎总是这样的做法保证你会看到它为0,0,永不1,0

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

采取的printf的签名:

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

这些省略号表示未知数量的参数。如果参数是从左到右推动的,那么格式将位于我们不知道大小的堆栈的底部。

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

但是,即使这样也无法为您节省,因为大多数C编译器都可以选择分析函数“Pascal风格”。而这一切意味着函数参数以从左到右的方式压入堆栈。例如,如果printf是用Pascal选项编译的,那么输出很可能是1,0(但是,因为printf使用椭圆,我不认为它可以编译为Pascal样式)。

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被设置为新值。

1

嗯,现在的OP已经被编辑过的一致性,它不与答案不同步。关于评估顺序的基本答案是正确的。但是,foo(++ test,test)的具体可能值是不同的;案件。

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

1

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

通常,编译器将不会改变其行为的这种类型的事情(除了重大修改)。

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

1

要重复别人怎么说,这是不明确的行为,而是不确定的。这个程序可以合法地输出任何东西或任何东西,将n留在任何值,或发送侮辱性的电子邮件给你的老板。

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

总之,不要这样做。如果你很好奇,在不同情况下测试它,但不要假装有一个正确或甚至可预测的结果。