发布增量运算符的行为

时间:2020-03-06 14:24:08  来源:igfitidea点击:
Possible Duplicate:

  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"
是什么赋予了??

解决方案

这是未指定行为的示例。该标准没有说明应以何种顺序对参数进行求值。这是编译器的实现决策。编译器可以按任意顺序自由评估函数的参数。

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

通常,在参数中产生副作用是不好的编程习惯。

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

从语义上讲,它等同于我们要完成的工作。

编辑:
正如Anthony正确指出的那样,在没有中间序列点的情况下,读取和修改单个变量都是不确定的。因此,在这种情况下,行为确实是不确定的。因此,编译器可以自由生成所需的任何代码。

编译器可能未按照我们期望的顺序评估参数。

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

实际应该在调用foo()之后在单独的语句中完成增量。即使行为是在C / C ++标准中指定的,也会造成混淆。我们可能会认为C ++编译器会将其标记为潜在错误。

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

<----错误开始WRONG WRONG ---->

调用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 1"或者" 0 0"。顺序可以随编译器而变化,并且同一编译器可以基于优化参数选择不同的顺序。

编写foo(test,test + 1)然后在下一行执行++ test是更安全的。无论如何,编译器应尽可能对其进行优化。

这是"未指定的行为",但是实际上在指定C调用堆栈的方式中,它几乎总是保证我们将其视为0、0,而不是1、0。

有人指出,VC的汇编器输出首先将最右边的参数压入堆栈。这就是在汇编器中实现C函数调用的方式。这是为了适应C的"无尽参数列表"功能。通过按从右到左的顺序推入参数,可以确保第一个参数是堆栈中的第一项。

取得printf的签名:

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

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

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

但是,即使这样也不能为我们省钱,因为大多数C编译器都可以选择解析函数" Pascal样式"。这意味着将函数参数以从左到右的方式压入堆栈。例如,如果使用Pascal选项编译了printf,则输出很可能为1、0(但是,由于printf使用椭圆,所以我认为它不能编译为Pascal样式)。

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

是的,没有指定参数评估的顺序,但是在没有中间顺序点的情况下,读取和修改单个变量都没有定义,除非读取仅是为了计算新值。函数参数的评估之间没有序列点,因此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'设置为新值。

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

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

根据C标准,在单个序列点中有多个引用变量(在这里我们可以将其视为语句或者函数参数)是一种不确定的行为,其中多个引用之一修改前/修改后。
所以:
foo(f ++,f)<-关于f何时递增的未定义。
同样(我一直在用户代码中看到这一点):

  • p = p ++ + p;

通常,编译器不会针对此类事件更改其行为(主要版本除外)。

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

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

作为实践,编译器编写者通常只会做他们最容易编写的事情,这通常意味着程序将获取n一次或者两次,调用函数并在某个时候递增。就像任何其他可能的行为一样,按照标准,这也很好。没有理由期望编译器或者版本之间具有相同的行为,或者具有不同的编译器选项。尽管我敢打赌,没有理由为什么必须在同一程序中一致编译两个不同但外观相似的示例。

简而言之,不要这样做。如果我们感到好奇,请在不同的环境下进行测试,但不要假装只有一个正确甚至可预测的结果。