在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 */
}
7条答案
按热度按时间zzzyeukh1#
不,函数参数在C中不按定义的顺序求值。
请参阅Martin约克对What are all the common undefined behaviour that c++ programmer should know about?的回答。
xdnvmnnf2#
函数参数的求值顺序未指定,参见C99 §6.5.2.2p10:
函数指示符、实际参数和实际参数中的子表达式的求值顺序未指定,但在实际调用之前有一个序列点。
C89中也有类似的措辞。
此外,您多次修改
pa
,但没有插入序列点,这会调用未定义的行为(逗号操作符引入序列点,但分隔函数参数的逗号没有)。如果您在编译器上打开警告,它应该会警告您:t3psigkw3#
只是为了增加一些经验。
下面的代码:
导致
在Linux上使用g++ 4.2.1。i686
1 2 3
-在Linux上使用SunStudio C++ 5.9。i6862 1 3
-在SunOS上使用g++ 4.2.1。x86 pc1 2 3
-在SunOS上使用SunStudio C++ 5.9。x86 pc1 2 3
-在SunOS上使用g++ 4.2.1。1 2 3
-在操作系统上使用C++ 5.9。m4pnthwp4#
在C中调用函数时,是否可以假定函数参数的求值顺序?
不,如果是unspecified behavior,则不能假定,
6.5
部分3
中的draft C99 standard表示:运算符和操作数的分组由语法指示。74)除了后面指定的(对于函数调用(),&&,||、?:和逗号运算子),未指定子运算式的评估顺序和副作用发生的顺序。
它还说,除了后面特别指定的站点
function-call ()
,所以我们看到,稍后在标准草案6.5.2.2
Function calls10
部分中,段落10
说:未指定函数指示符、实际参数和实际参数中的子表达式的求值顺序,但在实际调用之前有一个序列点。
此程序还显示undefined behavior,因为您在sequence points之间多次修改
pa
。根据草案标准第6.5
节第2
段:在上一个和下一个序列点之间,对象的存储值最多只能通过表达式求值修改一次。此外,前一个值应为只读,以确定要存储的值。
它引用了以下未定义的代码示例:
值得注意的是,虽然comma operator确实引入了序列点,但函数调用中使用的逗号是分隔符而不是
comma operator
。如果我们看6.5.17
节 * 逗号运算符 *2
段,可以看出:逗号运算符的左操作数计算为void表达式;在其求值之后存在序列点。
但是第
3
段说:示例如语法所示,逗号运算符(如本子条款中所述)不能出现在使用逗号分隔列表中项目的上下文中(如函数的参数或初始化程序列表)。
如果不知道这一点,则至少使用
-Wall
打开gcc
警告将提供类似于以下内容的消息:默认情况下,
clang
将发出警告,并显示类似以下内容消息:通常,了解如何以最有效的方式使用工具是很重要的,了解可用于警告的标志也很重要,对于
gcc
,您可以找到here的信息。一些有用的标志,从长远来看可以为您节省很多麻烦,并且是gcc
和clang
所共有的标志是-Wextra -Wconversion -pedantic
。对于clang
,理解-fsanitize非常有帮助。例如,-fsanitize=undefined
将在运行时捕获许多未定义行为的示例。sxissh065#
正如其他人已经说过的,函数参数的求值顺序是未指定的,并且求值之间没有序列点。因为在传递每个参数时,你随后更改了
pa
,所以在两个序列点之间更改和读取了pa
两次。这实际上是未定义的行为。我在愚者手册中找到了一个非常好的解释,我认为这可能会有帮助:C和C标准根据序列点定义了C/C程序中表达式的求值顺序,序列点表示程序各部分执行之间的偏序:在序列点之前执行的那些,以及在序列点之后执行的那些。这些发生在完整表达式(不是更大表达式的一部分的表达式)的求值之后,在&&的第一个操作数的求值之后,||,?:或者,(逗号)运算符,在调用函数之前(但是在它的参数和表示被调用函数的表达式求值之后),以及在某些其他地方。除了由序列点规则表示的,表达式的子表达式求值的顺序是不指定的。所有这些规则只描述了部分顺序而不是全部顺序,因为,例如,如果两个函数在一个表达式中被调用,而它们之间没有序列点,那么函数被调用的顺序是不指定的。2但是,标准委员会已经规定函数调用不能重叠。
在序列点之间对对象值的修改何时生效没有指定。其行为依赖于此的程序具有未定义的行为; C和C标准规定“在前一个和下一个序列点之间,对象的存储值最多只能通过表达式的求值修改一次。此外,前一个值只能被读取以确定要存储的值。"如果程序违反了这些规则,则任何特定实现的结果都是完全不可预测的。
具有未定义行为的代码的示例是a = a; a[n] = B[n++]且a[i++] = i;一些更复杂的情况不能通过该选项诊断,并且它可能给予偶然的假阳性结果,但是通常已经发现它在检测程序中的这类问题方面相当有效。
该标准的措辞令人困惑,因此在一些微妙的情况下,对于序列点规则的确切含义存在一些争论。有关该问题的讨论,包括提出的正式定义,可以在愚者阅读页面上找到链接,地址为http://gcc.gnu.org/readings.html。
2q5ifsrm6#
在表达式中多次修改变量是未定义的行为。因此,在不同的编译器上可能会得到不同的结果。因此,请避免多次修改变量。
2hh7jdfx7#
格兰特的答案是正确的,它是未定义的。
但是...
从你的例子来看,你的编译器似乎是按照从右到左的顺序进行计算的(毫不奇怪,就是把参数压入堆栈的顺序)。如果你可以做其他测试来证明即使启用了优化,这个顺序也是一致的,并且如果你只打算坚持使用那个版本的编译器,你就可以安全地假设从右到左的顺序。
这是完全不可移植的,一个可怕的,可怕的事情做,虽然。