在 C 中调用函数之前的参数计算顺序

在 C 语言中调用它时,是否可以假定函数参数的求值顺序?根据下面的程序,似乎没有一个特定的顺序,当我执行它。

#include <stdio.h>


int main()
{
int a[] = {1, 2, 3};
int * pa;


pa = &a[0];
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa), *(pa++),*(++pa));
/* Result: a[0] = 3  a[1] = 2    a[2] = 2 */


pa = &a[0];
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa++),*(pa),*(++pa));
/* Result: a[0] = 2  a[1] = 2     a[2] = 2 */


pa = &a[0];
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa++),*(++pa), *(pa));
/* a[0] = 2  a[1] = 2 a[2] = 1 */


}
51760 次浏览

不,函数参数不是按照 C 中定义的顺序计算的。

参见 Martin York 对 C + + 程序员应该知道的常见的未定义行为是什么?的回答。

函数参数的求值顺序未指定,来自 C996.5.2.2 p10:

类的计算顺序 函数指示符,实际的 参数和子表达式 实际参数是未指定的, 但是之前有一个序列点 真正的电话。

C89中也有类似的措辞。

此外,你可以多次修改 pa,而不需要中间的序列点来调用未定义行为(逗号操作符引入序列点,但是分隔函数参数的逗号不引入序列点)。如果你打开你的编译器的警告,它应该警告你:

$ gcc -Wall -W -ansi -pedantic test.c -o test
test.c: In function ‘main’:
test.c:9: warning: operation on ‘pa’ may be undefined
test.c:9: warning: operation on ‘pa’ may be undefined
test.c:13: warning: operation on ‘pa’ may be undefined
test.c:13: warning: operation on ‘pa’ may be undefined
test.c:17: warning: operation on ‘pa’ may be undefined
test.c:17: warning: operation on ‘pa’ may be undefined
test.c:20: warning: control reaches end of non-void function

格兰特的回答是正确的,没有定义。

但是..,

在您的示例中,您的编译器似乎是按照从右到左的顺序进行计算的(毫不奇怪,是将参数推送到堆栈上的顺序)。如果您可以通过其他测试来显示即使启用了优化,顺序也保持一致,并且如果您只打算使用那个版本的编译器,那么您可以安全地假设顺序是从右到左的。

这是完全不可携带的,这是一件非常非常可怕的事情。

正如其他人已经说过的,计算函数参数的顺序是未指定的,并且在计算它们之间没有序列点。因为随后在传递每个参数时更改 pa,所以在两个序列点之间更改并读取 pa两次。真是未定义行为。我在海湾合作委员会的手册中找到了一个很好的解释,我认为可能会有帮助:

C 和 C + + 标准定义了 C/C + + 程序中表达式按照序列点求值的顺序,序列点表示程序各部分执行之间的部分顺序: 在序列点之前执行的部分和在序列点之后执行的部分。这些发生在对完整表达式(不是较大表达式的一部分的表达式)求值之后,在对 a & ,| | ,? 的第一个操作数求值之后?: 或(逗号)运算符,在函数被调用之前(但在其参数和表示被调用函数的表达式求值之后) ,以及在某些其他地方。除了由序列点规则表示外,没有指定表达式子表达式的求值顺序。所有这些规则只描述部分顺序,而不是总顺序,因为,例如,如果在一个表达式中调用两个函数,它们之间没有序列点,则不指定调用函数的顺序。然而,标准委员会已经裁定函数调用不重叠。

在序列点之间对对象值的修改生效时不指定。行为依赖于此的程序有未定义行为,C 和 C + + 标准规定“在前一个序列点和下一个序列点之间,一个对象的存储值应该通过表达式的求值最多修改一次。此外,先前的值应该被读取,只是为了确定要存储的值。”.如果程序违反了这些规则,任何特定实现的结果都是完全不可预测的。

有未定义行为的代码有 a = a + + ,a [ n ] = b [ n + + ]和 a [ i + + ] = i;。一些较为复杂的病例不能用这种方法诊断,偶尔可能会出现假阳性结果,但总的来说,在程序中发现这种方法对于检测这类问题是相当有效的。

该标准措辞混乱,因此,在一些微妙的情况下,对序列点规则的确切含义存在一些争议。有关这个问题的讨论,包括提议的正式定义的链接,可以在 http://gcc.gnu.org/readings.html的海湾合作委员会读物页面上找到。

只是为了增加一些经验。
以下代码:

int i=1;
printf("%d %d %d\n", i++, i++, i);

结果出来了

2 1 3-在 Linux.i686上使用 g + + 4.2.1
1 2 3-在 Linux.i686上使用 SunStudio C + + 5.9
2 1 3-在 SunOS.x86pc 上使用 g + + 4.2.1
1 2 3-在 SunOS.x86pc 上使用 SunStudio C + + 5.9
在 SunOS.sun4u 上使用 g + + 4.2.1
在 SunOS.sun4u 上使用 SunStudio C + + 5.9

在 C 语言中调用它时,是否可以假定函数参数的求值顺序?

不,不能假设,如果是 未指明的行为6.53段中的 C99标准草案说:

运算符和操作数的分组由语法表示。74)除非指定 稍后(对于函数调用() ,& & ,| | ,?和逗号运算符) ,< strong > 子表达式的求值顺序和副作用发生的顺序都未指定。

它还说,除了指定的以后,特别是网站 function-call (),所以我们看到,在后来的标准草案 6.5.2.2 函数调用段落 10说:

函数指示符、实际参数和 实际参数中的子表达式未指定 ,但是有一个序列点 在真正打电话之前。

这个程序还展示了 未定义行为,因为您在 序列点之间修改 pa不止一次。来自标准草案第 6.5节第 2段:

在前一个序列点和下一个序列点之间,一个对象应该有它的存储值 通过计算一个表达式最多修改一次 只读取以确定要存储的值。

它引用了以下未定义的代码示例:

i = ++i + 1;
a[i++] = i;

需要注意的是,尽管 逗号运算符确实引入了序列点,但函数调用中使用的逗号是一个分隔符,而不是 comma operator。如果我们看一下 6.5.17逗号运算符2说:

逗号运算符的左操作数被计算为 void 表达式; < strong > 有一个 序列点后进行评估

3段说:

如语法所示,逗号运算符(如子句中所述) 不能出现在逗号用于分隔列表中的项的上下文中(例如函数的参数或初始值设定项列表)

在不知道这一点的情况下,使用至少使用 -Wallgcc打开警告会提供类似于下面这样的消息:

warning: operation on 'pa' may be undefined [-Wsequence-point]
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa), *(pa++),*(++pa));
^

默认情况下,clang会发出类似下列信息的警告:

warning: unsequenced modification and access to 'pa' [-Wunsequenced]
printf("a[0] = %d\ta[1] = %d\ta[2] = %d\n",*(pa), *(pa++),*(++pa));
~         ^

一般来说,了解如何以最有效的方式使用工具非常重要,了解可用于警告的标志非常重要,对于 gcc,您可以找到这些信息 给你-Wextra -Wconversion -pedanticgccclang共有的一些标志,它们非常有用,从长远来看可以为您节省很多麻烦。对于 clang 理解-美化可以非常有帮助。例如,-fsanitize=undefined会在运行时捕获许多未定义行为的实例。

修改一个表达式中的变量不止一次是未定义行为。 因此,您可能会在不同的编译器上得到不同的结果。