当我遗漏 C 函数的参数时会发生什么?

What happens when I leave out an argument to a function in C?

首先,我知道这种编程方式不是很好的做法。有关我为什么这样做的解释,请在实际问题后继续阅读。

像这样在 C 中声明函数时:

int f(n, r) {…}

rn的类型将默认为int。编译器可能会生成警告,但我们选择忽略它。

现在假设我们调用 f 但是,无意或以其他方式遗漏了一个参数:

f(25);

这仍然会编译 just fine(用 gcc 和 clang 测试)。但是 gcc 没有关于缺少参数的警告。

所以我的问题是:

  1. 为什么这不会产生警告(在 gcc 中)或错误?
  2. 执行时到底发生了什么?我假设我正在调用未定义的行为,但我仍然希望得到解释。

请注意,当我声明 int f(int n, int r) {…} 时,gcc 和 clang 都不是这样工作的 will compile this

现在如果你想知道为什么我会做这样的事情,我正在玩 Code Golf and tried to shorten my code,它使用了递归函数 f(n, r)。我需要一种隐式调用 f(n, 0) 的方法,所以我定义了 F(n) { return f(n, 0) },这对我来说有点太多字节了。所以我想知道我是否可以省略这个参数。我不能,它仍然可以编译,但不再有效。

在优化这段代码时,有人向我指出,我可以在函数末尾省略一个 return——gcc 也没有对此发出警告。 gcc 是不是太宽容了?

  1. 您没有从编译器获得任何诊断,因为您没有使用现代 "prototyped" 函数声明。如果你写了

    int f(int n, int r) {…}
    

    然后随后的 f(25) 触发诊断。使用我正在输入的计算机上的编译器,这实际上是一个硬错误。

    "Old-style" 函数声明和定义故意导致编译器放宽其许多规则,因为 old-style code 它们的存在是为了向后兼容会一直做这样的事情。不是你想要做的事情,希望 f(25) 会以某种方式被解释为 f(25, 0),但是,例如,f(25) 其中 f 的 body当 n 参数为 25 时,从不查看 r 参数。

  2. 评论你的问题的书呆子们说字面上任何事情都可能发生(无论如何,在计算机的物理能力范围内;"demons will fly out of your nose" 是典型的笑话,但它实际上是一个笑话)。但是,可以描述 通常 发生的两种一般 类 事情。

    对于较旧的编译器,通常会为 f(25) 生成代码,就像 f 只接受一个参数时一样。这意味着 f 将在其中查找其 second 参数的内存或寄存器位置未初始化,并且包含一些垃圾值。

    另一方面,对于较新的编译器,编译器很容易观察到任何通过 f(25) 的 control-flow 路径具有未定义的行为,并且基于该观察,假设所有这些 control-flow 路径从未被采用 ,并删除它们。是的,即使它是程序中唯一的 control-flow 路径。我亲眼目睹了 Clang 吐出 main: ret 一个程序,其所有 control-flow 路径都有未定义的行为!

  3. GCC 不抱怨 f(n, r) { /* no return statement */ } 是另一种情况,如 (1),其中 old-style 函数定义放宽了规则。 void是1989年C标准发明的;在此之前,没有办法明确地说函数不是 return 值。所以你得不到诊断,因为编译器无法知道你不是故意的。

    独立于此,是的,GCC 的默认行为 以现代标准来看是非常宽松的。那是因为 GCC 本身比 1989 年的 C 标准更早,并且 body 已经很长时间没有重新检查它的默认行为了。对于新程序,您应该始终使用 -Wall,我还建议至少尝试使用 -Wextra-Wpedantic-Wstrict-prototypes-Wwrite-strings。事实上,我建议阅读手册的 "Warning Options" 部分并尝试使用 all 的附加警告选项。 (但是请注意,您应该 使用 -std=c11,因为这有破坏系统 headers 的严重倾向。请改用 -std=gnu11。)

首先,C 标准不区分警告和错误。它只谈论"diagnostics"。特别是,编译器始终可以在不违反标准的情况下生成可执行文件(即使源代码完全损坏)。1

The types of r and n will default to int.

不再是了。自 1999 年以来,隐式 int 已从 C 中消失。(并且您的测试代码需要 C99,因为 for (int i = 0; ... 在 C90 中无效)。

在您的测试代码中,gcc 确实为此发出诊断:

.code.tio.c: In function ‘f’:
.code.tio.c:2:5: warning: type of ‘n’ defaults to ‘int’ [-Wimplicit-int]

它不是有效代码,但 gcc 仍会生成可执行文件(除非您启用 -Werror)。

如果您添加所需的类型 (int f(int n, int r)),它会发现下一个问题:

.code.tio.c: In function ‘main’:
.code.tio.c:5:3: error: too few arguments to function ‘f’

这里 gcc 有点武断地决定不生成可执行文件。

C99 的相关引述(可能还有 C11;此文本在 n1570 draft 中未更改):

6.9.1 Function definitions

Constraints

[...]

  1. If the declarator includes an identifier list, each declaration in the declaration list shall have at least one declarator, those declarators shall declare only identifiers from the identifier list, and every identifier in the identifier list shall be declared.

您的代码违反了约束(您的函数声明符包含一个标识符列表,但没有声明列表),这需要诊断(例如来自 gcc 的警告)。

Semantics

  1. [...] If the declarator includes an identifier list, the types of the parameters shall be declared in a following declaration list.

您的代码违反了此 规则,因此它具有未定义的行为。即使从未调用该函数,这也适用!

6.5.2.2 Function calls

Constraints

[...]

  1. If the expression that denotes the called function has a type that includes a prototype, the number of arguments shall agree with the number of parameters. [...]

Semantics

[...]

  1. [...] If the number of arguments does not equal the number of parameters, the behavior is undefined. [...]

如果传递的参数数量与函数的参数数量不匹配,则实际调用也有未定义的行为。

至于省略return:只要调用者不看返回值,这个其实是有效的

参考(6.9.1 函数定义,语义):

  1. If the } that terminates a function is reached, and the value of the function call is used by the caller, the behavior is undefined.

1 唯一的例外似乎是 #error 指令,标准说:

The implementation shall not successfully translate a preprocessing translation unit containing a #error preprocessing directive unless it is part of a group skipped by conditional inclusion.