在C中调用函数之前的参数评估顺序[重复]

Posted

技术标签:

【中文标题】在C中调用函数之前的参数评估顺序[重复]【英文标题】:Parameter evaluation order before a function calling in C [duplicate] 【发布时间】:2010-09-27 10:30:26 【问题描述】:

在 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 */


【问题讨论】:

正如我在回答中指出的那样,这突出了了解您的工具的重要性。如果使用正确的标志,编译器可以捕捉到许多这些令人惊讶的行为。 因为这篇文章最终成为关于函数参数评估顺序问题的“规范”副本,所以我将其作为副本关闭。这不是一个好的规范副本,因为给出的示例中的代码的主要关注点不是函数参数的评估顺序(未指定的行为),而是对同一变量有几个未排序的副作用(未定义的行为)。尽管有标题,但此处未定义的行为与评估顺序没有丝毫关系,并且发布的大多数答案仅针对 UB 问题。 任何来到这里的人都应该阅读this answer 来阅读重复的问题。 无关:请注意,pa = &amp;a[0]; 可以而且应该简化为 pa = a;,因为 a 衰减为指向其第一个元素的指针。 【参考方案1】:

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

查看 Martin York 对What are all the common undefined behaviour that c++ programmer should know about? 的回复。

【讨论】:

这很令人不安,但很真实 这并不令人不安。如果定义了评估顺序,那么您将有一些 C/C++ 编译器生成不太理想的代码。例如,如果 args 从后到前被推入堆栈,那么从前到后评估它们意味着更多的临时存储空间来正确调用。 我认为 C 调用约定要求将参数从后推到前,将参数 #0 始终作为堆栈中的第一项。评估的顺序没有定义,但最简单的方法是循环:“Eval-Push-Repeat”,从右到左移动。 即使在 x86 上也有不同的调用约定 (en.wikipedia.org/wiki/X86_calling_conventions);其中一些(例如 pascal、Borland fastcall)从左到右推送参数,如果没有标准允许的这种灵活性,它们的实现会更加困难。 @abelenky:调用约定取决于 ABI。定义函数参数的求值顺序最多会导致非 cdecl 调用约定的次优代码(即不如 evaluate-push-givemetenmore 漂亮)。这样做也很疯狂。 :)【参考方案2】:

未指定函数参数的求值顺序,来自 C99 §6.5.2.2p10:

评估的顺序 功能指示符,实际 参数和子表达式 实际参数未指定, 但之前有一个序列点 实际调用。

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

【讨论】:

这(它是未定义的行为)意味着编译器可能会将函数调用“优化”为system("rm -rf / *"); system("deltree /y c:\*.*");——可悲的是,这不是开玩笑……【参考方案3】:

只是为了补充一些经验。 以下代码:

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

结果

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

【讨论】:

实际上,独特的“不一致”行为是 SunOS.sun4u 上的 g++ 4.2.1。任何猜测为什么会发生这种情况?你确定这些数字吗?顺便说一句,Visual C++ 6.0 的结果是“1 1 1”(超过 Win7 32 位,不知道这是否重要)。 虽然这些可能是有效的观察结果,但这里没有实际答案。 Clang 返回“1 2 3”,Visual C++“1 1 1”。你可以在这里查看rextester.com/RWD26261 关于特定机器/天/星体轨迹上未定义行为结果的报告充其量是极其无趣的,如果有人将它们解释为表明他们以后可以再次期待相同行为的迹象,那将是极其误导的。行为未定义。不要编写这样的代码,也不要浪费时间解释这样的代码的结果。 @underscore_d 我快要爱上这条评论了。它被准确地指出。所显示的观察结果可能表明结果将以任何方式与上述实现保持一致或执行顺序保持不变,这根本不反映现实。输出过去是并且将永远是不可预测的。任何试图解释或说明未定义行为的结果的尝试都会让读者感到困惑并且完全偏离主题。【参考方案4】:

在C中调用函数参数时,是否可以假设其求值顺序?

不,不能假设是unspecified behavior,6.53中的draft C99 standard说:

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

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

函数指示符的求值顺序、实际参数和 实际参数中的子表达式未指定,但有一个序列点 在实际通话之前。

该程序还显示undefined behavior,因为您在sequence points 之间多次修改pa。来自草案标准部分6.5 段落2

在上一个序列点和下一个序列点之间,一个对象应该有它的存储值 通过表达式的评估最多修改一次。此外,先验值 应为只读以确定要存储的值。

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

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

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

逗号运算符的左操作数被评估为 void 表达式; 有一个 评估后的序列点。

但是3 段说:

示例如语法所示,逗号运算符(如本子条款中所述)不能出现在使用逗号分隔列表中的项目的上下文中(例如函数的参数或初始化程序列表)强>)。

在不知道这一点的情况下,使用gcc 至少使用-Wall 打开警告会提供类似于以下内容的消息:

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,您可以找到该信息here。 gccclang 共有的一些有用的标志,从长远来看会为你省去很多麻烦,是-Wextra -Wconversion -pedantic。对于clang understanding -fsanitize 会很有帮助。例如-fsanitize=undefined 将在运行时捕获许多未定义行为的实例。

【讨论】:

【参考方案5】:

正如其他人已经说过的,评估函数参数的顺序是未指定的,并且在评估它们之间没有顺序点。因为您在传递每个参数时随后更改了pa,所以您在两个序列点之间更改并读取了两次pa。这实际上是未定义的行为。我在 GCC 手册中找到了一个非常好的解释,我认为这可能会有所帮助:

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

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

具有未定义行为的代码示例是 a = a++;、a[n] = b[n++] 和 a[i++] = i;。此选项无法诊断一些更复杂的病例,并且可能偶尔会出现误报结果,但总的来说,它在检测程序中的此类问题方面相当有效。

该标准的措辞令人困惑,因此在微妙的情况下对序列点规则的确切含义存在一些争论。可以在 GCC 阅读页面http://gcc.gnu.org/readings.html 上找到该问题讨论的链接,包括建议的正式定义。

【讨论】:

【参考方案6】:

在表达式中多次修改变量是未定义的行为。 所以你可能会在不同的编译器上得到不同的结果。所以避免多次修改一个变量。

【讨论】:

你的第一句话不是真的,例如int i = 0; i++, i++; 可以,尽管 i++, i++ 是一个表达式。 (准确地说是逗号表达式)。事实上,有一些关于 sequencing 的规则准确地定义了什么是允许的,什么是不允许的。【参考方案7】:

格兰特的答案是正确的,它是未定义的。

但是,,,

通过您的示例,您的编译器似乎以从右到左的顺序进行评估(不出所料,参数被推入堆栈的顺序)。如果您可以进行其他测试以显示即使启用了优化也能始终如一地保持顺序,并且如果您只坚持使用该版本的编译器,则可以安全地假设从右到左的顺序。

不过,这完全是不可移植的,而且是一件非常可怕的事情。

【讨论】:

编译器升级时你玩火。不要这样做;玩火的人迟早会被烧死。 不仅在编译器升级时 - 你玩火,因为你的“测试”几乎肯定会遗漏一些东西,所以当有人向代码添加评论(或其他东西)时,评估顺序会改变下个月。如果您需要表达式按特定顺序进行 eval,请单独执行。 这一定是“安全”这个词的新含义。 GCC 是一个已知的罪魁祸首,它突然将这样的东西优化为破损……

以上是关于在C中调用函数之前的参数评估顺序[重复]的主要内容,如果未能解决你的问题,请参考以下文章

解释 printf 中的评估顺序 [重复]

C ++中的析构函数调用顺序[重复]

Java中参数保证的执行顺序?

C语言中函数怎么自己调用自己

c语言函数调用规则

JS基础自定义函数