【问题标题】:function parameter evaluation order函数参数求值顺序
【发布时间】:2012-03-05 11:54:29
【问题描述】:

在 C 和 C++ 中,对函数的参数求值是否有固定的顺序?我的意思是,标准是怎么说的?是left-to-right 还是right-to-left? 我从书中得到了令人困惑的信息。

function call 是否需要使用stack only 来实现? C 和 C++ 标准对此有何规定?

【问题讨论】:

  • 标准没有具体说明。顺序甚至可以在运行时随机更改(但没有实现这样做,AFAIK)。
  • 小注意——你可以认为逗号是一个序列点,它会强制执行从左到右的排序。但是逗号操作符是一个序列点,不是分隔函数参数的逗号。
  • 如果您依赖它,那么您的代码可能难以阅读、推理和维护。
  • SO 上无数重复的问题中缺少哪些信息?
  • 不要同时用 C 和 C++ 标记您的问题,如果它不是关于差异、相似之处、应该适用于两者的代码或类似的东西。

标签: c++ c


【解决方案1】:

C 和 C++ 是两种完全不同的语言;不要假设相同的规则总是适用于两者。但是,在参数评估顺序的情况下:

C99:

6.5.2.2 函数调用
...
10 函数指示符的评估顺序、实际参数和 实际参数中的子表达式未指定,但有一个序列点 在实际通话之前。

[编辑] C11(草案):

6.5.2.2 函数调用
...
10 功能指示符和实际值评估后有一个序列点 参数,但在实际调用之前。调用函数中的每个评估(包括 其他函数调用)在之前或之后没有特别排序的 被调用函数体的执行顺序是不确定的 被调用函数的执行。94)
...
94) 换句话说,函数执行不会相互“交错”。

C++:

5.2.2 函数调用
...
8 参数的评估顺序未指定。参数表达式求值的所有副作用生效 在输入函数之前。后缀表达式和参数表达式列表的求值顺序是 未指定。

这两个标准都没有强制要求使用硬件堆栈来传递函数参数;这是一个实现细节。 C++ 标准使用术语“展开堆栈”来描述在从 try 块到 throw-expression 的路径上为自动创建的对象调用析构函数,但仅此而已。大多数流行的架构确实通过硬件堆栈传递参数,但它不是通用的。

[编辑]

我从书中得到了令人困惑的信息。

这一点也不奇怪,因为 90% 的关于 C 的书籍很容易垃圾

虽然语言标准对于学习 C 或 C++ 而言都不是很好的资源,但最好能方便地解决此类问题。官方™ 标准文档需要花费真金白银,但有些草稿可以在线免费获得,对于大多数用途来说应该足够了。

最新的 C99 草案(自原始发布以来有更新)可通过here 获得。最新的预发布 C11 草案(去年正式批准)可在here 获得。可以通过here 获得公开可用的 C++ 语言草稿,尽管它明确声明某些信息不完整或不正确。

【讨论】:

  • "C++ 标准使用术语“展开堆栈”——我同意你的观点,我认为“展开堆栈”是指通过调用堆栈退出, 从概念上讲,它是一个堆栈,不管它在内存中的实际表示方式,也不管函数参数的地址如何与实现可能用来表示调用堆栈的任何结构相关。所以术语“展开堆栈”并不损害调用约定,实际上许多调用约定在可能的情况下至少使用一些寄存器。
  • 因为 90% 的书很容易写出来...您是否还知道,使用统计数据来断言其真实性的陈述中有 95% 是当场捏造的,而且除了表征其他统计数据之外完全没用吗? (顺便说一句很好的答案)
  • @John Bode “C 和 C++ 是两种完全不同的语言;不要假设相同的规则总是适用于两者。” C++ 编译器编译 C 代码,所以我们可以说 C 规则适用于 C++,我们不能吗?
  • @thoron:不。有很多合法的 C 程序不是合法的 C++ 程序,也有合法的 C 程序也是合法的 C++ 程序,但语义不同。这两种语言有很多共同点,但也有显着不同的地方。
【解决方案2】:

保持安全:标准将其留给编译器来确定评估参数的顺序。所以你不应该依赖于特定的订单被保留。

【讨论】:

  • 如果您删除了第二段,我会支持您。我只是觉得这很混乱。
  • @sad:您无法“解决”任何问题。当编译器优化你的代码做错事时,不要回来抱怨,因为你依赖于不可靠的假设。
  • 第二段是完全错误的。即使调用约定是以从右到左的顺序推送参数,指令重新排序也可以改变求值的顺序。
  • @R.MartinhoFernandes 如果观察到的行为受到影响,我很确定编译器在我的 sn-p 中不能在 foo 之前调用 goo
  • @Luchian 这不是第二段。
【解决方案3】:

在 C/C++ 中,对函数的参数求值有固定的顺序。我的意思是标准所说的是从左到右或从右到左。我从书中得到了令人困惑的信息。

不,函数参数(以及任何表达式中的两个子表达式)的求值顺序在 C 和 C++ 中是未指定的行为。用简单的英语来说,这意味着可以首先评估最左边的参数,也可以是最右边的参数,您无法知道哪个顺序适用于特定编译器

例子:

static int x = 0;

int* func (int val)
{
  x = val;
  return &x;
}

void print (int val1, int val2)
{
  cout << val1 << " " << val2 << endl;
}

print(*func(1), *func(2));

这段代码很糟糕。它依赖于打印参数的评估顺序。它将打印“1 1”(从右到左)或“2 2”(从左到右),我们不知道是哪个。该标准唯一保证的是对 func() 的调用都在调用 print() 之前完成。

对此的解决方案是注意顺序是未指定的,并编写不依赖于评估顺序的程序。例如:

int val1 = *func(1);
int val2 = *func(2);
print(val1, val2); // Will always print "1 2" on any compiler.

函数调用是否必须只使用堆栈来实现。 C/C++ 标准对此有何规定。

这被称为“调用约定”,标准没有规定任何内容。如何传递参数(和返回值)完全取决于实现。它们可以通过 CPU 寄存器或堆栈或其他方式传递。调用者可能是负责在堆栈上推送/弹出参数的人,也可能是函数负责。

函数参数的求值顺序仅与调用约定有一定的关联,因为求值发生在函数调用之前。但另一方面,某些编译器可以选择将最右边的参数放在 CPU 寄存器中,而将其余参数放在堆栈中,例如。

【讨论】:

    【解决方案4】:

    仅针对C语言而言,函数参数内部的求值顺序取决于编译器。出自 Brian KernighanDennis RitchieThe C Programming Language

    同样,函数参数的计算顺序不是 指定,所以声明

    printf("%d %d\n", ++n, power(2, n)); /*WRONG */

    可以用不同的编译器产生不同的结果, 取决于 n 在调用 power 之前是否增加。这 解决办法当然是写

    ++n;

    printf("%d %d\n", n, power(2, n));

    【讨论】:

      【解决方案5】:

      据我所知,函数printf这里有一个例外。

      对于普通函数foofoo(bar(x++), baz(++x)) 的求值顺序未定义。正确的!然而printf,作为一个省略号函数有一个更明确的评估顺序。

      实际上,标准库中的printf 没有关于已发送到的参数数量的信息。它只是试图从字符串占位符中找出变量的数量;即,来自字符串中百分比运算符 (%) 的数量。 C 编译器开始从最右边向左边推送参数;并且字符串的地址作为最后一个参数传递。由于没有关于参数数量的确切信息,printf 评估最后一个地址(字符串)并开始用堆栈中相应地址的值替换%(从左到右)。也就是说,对于像下面这样的printf

      {
          int x = 0;
          printf("%d %d %f\n", foo(x), bar(x++), baz(++x));
      }
      

      求值顺序为:

      1. x 加 1,
      2. 函数 baz 用x = 1 调用;返回值被压入堆栈,
      3. x = 1调用函数栏;返回值被压入堆栈,
      4. x 加 1,
      5. 函数 foo 使用x = 2 调用;返回值被压入堆栈,
      6. 字符串地址被压入堆栈。

      现在,printf 没有关于已发送到的参数数量的信息。而且,如果在编译时没有发出-Wall,编译器甚至不会抱怨参数数量不一致。那是;字符串可能包含 3 个%,但printf 行中的参数数量可以是 1、2、3、4、5,甚至可以只包含字符串本身,根本不包含任何参数。

      假设,字符串有 3 个占位符,并且您发送了 5 个参数 (printf("%d %f %s\n", k1, k2, k3, k4, k5))。如果关闭编译警告,编译器将不会抱怨参数数量过多(或占位符数量不足)。知道堆栈地址,printf就会;

      1. 把栈中的第一个整数宽度作为整数打印出来(不知道是不是整数),
      2. 处理堆栈中的下一个双倍宽度并将其打印为双精度(不知道它是否为双精度),
      3. 将堆栈中的下一个指针宽度视为字符串地址,并尝试在该地址打印字符串,直到找到字符串终止符(前提是指向的地址属于该进程;否则会引发分段错误)。
      4. 并忽略其余参数。

      【讨论】:

        猜你喜欢
        • 2019-10-10
        • 2011-02-25
        • 1970-01-01
        相关资源
        最近更新 更多