【问题标题】:What happens when I leave out an argument to a function in C?当我在 C 中遗漏一个函数的参数时会发生什么?
【发布时间】:2018-04-03 14:59:52
【问题描述】:

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

当像这样在 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 并试图缩短我的 code,它使用了递归函数 f(n, r)。我需要一种隐式调用f(n, 0) 的方法,所以我定义了F(n) { return f(n, 0) },这对我来说有点太多字节了。所以我想知道我是否可以省略这个参数。我不能,它仍然可以编译,但不再有效。

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

【问题讨论】:

  • 您说的是过时的 C 功能,编译器仍支持这些功能以实现向后兼容性。如果没有原型,可以使用任意数量和类型的参数调用函数。但是现在这在严肃的编码中已经没有位置了,所以要获得提示,尤其是打代码,最好在 codegolf.stackexchange.com 上询问?
  • “r 和 n 的类型将默认为 int” - 不在现代严格符合的编译器上。
  • 这仍然可以编译得很好 不是在符合任何几十年前的 C 标准的编译器上。
  • “执行时究竟会发生什么?我假设我正在调用未定义的行为,但我仍然希望得到解释”——关于这个措辞的一般建议:这是不可能请求的,或者充其量需要一个月的博客才能涵盖实际上任何事情发生的可能性。

标签: c function parameter-passing compiler-warnings undefined-behavior


【解决方案1】:
  1. 您没有从编译器获得任何诊断信息,因为您没有使用现代“原型”函数声明。如果你写了

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

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

    “旧式”函数声明和定义故意使编译器放宽它的许多规则,因为它们为了向后兼容而存在的旧式代码会做这样的事情该死的时间。不是您想要做的事情,希望f(25) 会以某种方式被解释为f(25, 0),但是,例如,f(25) 其中f 的主体从不查看r 参数,而它的@987654328 @ 参数是 25。

  2. 当他们说字面上任何事情都可能发生时,对你的问题发表评论的学究是正确的(无论如何,在计算机的物理能力范围内;“恶魔会从你的鼻子飞出”是规范的玩笑,但事实上,这是一个玩笑)。然而,我们可以描述两类通常会发生的事情。

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

    另一方面,对于较新的编译器,编译器可能会观察到任何通过f(25) 的控制流路径具有未定义的行为,并且基于该观察,假设所有此类控制流路径永远不会被采取,并删除它们。是的,即使它是程序中唯一的控制流路径。我实际上亲眼目睹了 Clang 为一个所有控制流路径都具有未定义行为的程序吐出 main: ret

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

    独立于此,是的,GCC 的默认行为按照现代标准非常宽松。那是因为 GCC 本身比 1989 年的 C 标准更早,而且很长一段时间没有人重新检查它的默认行为。对于新程序,您应该始终使用-Wall,我还建议至少尝试-Wextra-Wpedantic-Wstrict-prototypes-Wwrite-strings。事实上,我建议您阅读手册的“警告选项”部分并尝试使用所有的其他警告选项。 (但请注意,您应该不要使用-std=c11,因为这很容易破坏系统标头。请改用-std=gnu11。)

【讨论】:

  • C11 仍然允许从非 void 函数中省略 return。见 6.9.1/12。
  • @melpomene 呵呵,我不知道只有在实际使用返回值时才会触发未定义的行为。直到。
  • "这意味着f 将在其中查找其第二个参数的内存或寄存器位置未初始化,并且包含一些垃圾值。" 这也意味着,如果调用约定要求函数清除堆栈(参见例如stdcall),f 将弹出比调用者推送的更多值。这会在程序的剩余生命周期中使堆栈不平衡,这可能很短,因为返回地址将是垃圾,导致程序崩溃。
【解决方案2】:

首先,C 标准不区分警告和错误。它只谈论“诊断”。特别是,编译器始终可以生成可执行文件(即使源代码完全损坏)而不会违反标准。1

rn 的类型将默认为 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 函数定义

约束

[...]

  1. 如果声明符包含标识符列表,则声明列表中的每个声明应 至少有一个声明符,这些声明符应仅声明来自 标识符列表,并声明标识符列表中的每个标识符。

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

语义

  1. [...] 如果 声明器包含一个标识符列表,参数的类型应在一个 以下声明列表。

你的代码违反了这个shall规则,所以它有未定义的行为。即使函数从未被调用,这也适用!

6.5.2.2 函数调用

约束

[...]

  1. 如果表示被调用函数的表达式具有包含原型的类型,则 参数的数量应与参数的数量一致。 [...]

语义

[...]

  1. [...] 如果参数的数量不等于参数的数量,则 行为未定义。 [...]

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

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

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

  1. 如果到达终止函数的},并且函数调用的值被 调用者,行为未定义。

1 唯一的例外似乎是#error 指令,标准中关于它的规定:

实现不能成功翻译预处理翻译单元 包含 #error 预处理指令,除非它是被跳过的组的一部分 有条件的包含。

【讨论】:

  • 注:尽管隐含的 int 已从 1999 年标准中的语言中删除,但我不知道 任何 编译器没有实现它,或者为它发出硬错误,即使在一种模式下据称严格遵守 C99 或更高版本,除非您明确将特定诊断提升为错误(使用 -Werror=implicit-int 或等效项)。
猜你喜欢
  • 1970-01-01
  • 2011-06-19
  • 2016-09-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多