【问题标题】:Why does gcc allow arguments to be passed to a function defined to be with no arguments?为什么 gcc 允许将参数传递给定义为没有参数的函数?
【发布时间】:2012-09-20 12:38:58
【问题描述】:

我不明白为什么这段代码可以编译?

#include <stdio.h>
void foo() {
    printf("Hello\n");
}

int main() {
    const char *str = "bar";
    foo(str);
    return 0;
}

gcc 甚至没有警告我向 foo() 传递了太多参数。这是预期的行为吗?

【问题讨论】:

  • 欢迎使用 C 语言,这是遗留标准的一部分。仅供参考,它在 C++ 中不受支持。
  • 如果您将函数声明为void foo(void),通常可以激发编译器生成警告
  • @HansPassant:如果您将其声明为void foo(void),编译器通常应该给出错误,而不仅仅是警告(尽管官方措辞只需要“诊断”)。
  • 编译时添加-Wall
  • @reddragon 见my answer

标签: c gcc compiler-errors


【解决方案1】:

C99 标准 (6.7.5.3) 和 C11 标准 (6.7.6.3) 状态:

标识符列表仅声明参数的标识符 的功能。函数声明器中的一个空列表,它是一部分 该函数的定义指定该函数没有 参数。函数声明器中的空列表不是一部分 该函数的定义指定没有关于 提供了参数的数量或类型。

由于 foo 的声明是定义的一部分,因此声明 指定 foo 接受 0 个参数,因此调用 foo(str) 位于 至少在道德上是错误的。但如下所述,有不同的 C 中的“错误”程度,编译器在处理方式上可能有所不同 带有某些“错误”。

举一个稍微简单的例子,考虑下面的程序:

int f() { return 9; }
int main() {
  return f(1);
}

如果我使用 Clang 编译上述内容:

tmp$ cc tmp3.c
tmp3.c:4:13: warning: too many arguments in call to 'f'
  return f(1);
         ~  ^
1 warning generated.

如果我使用 gcc 4.8 编译,即使使用 -Wall,我也不会收到任何错误或警告。先前的答案建议使用 -Wstrict-prototypes,它正确报告 f 的定义不是原型形式,但这真的不是重点。 C 标准允许以非原型形式定义函数,例如上述形式,并且标准明确规定该定义指定函数采用 0 个参数。

现在有一个约束(C11 Sec. 6.5.2.2):

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

但是,此约束不适用于这种情况,因为函数的类型不包括原型。但这是语义部分中的后续声明(不是“约束”),它确实适用:

如果表示被调用函数的表达式的类型不包含原型 ...如果参数的数量不等于参数的数量,则行为未定义。

因此函数调用确实会导致未定义的行为(即,程序不是“严格符合”的)。但是,该标准仅要求实现在违反实际约束时报告诊断消息,在这种情况下,没有违反约束。因此,gcc 不需要报告错误或警告即可成为“符合要求的实现”。

所以我认为这个问题的答案,为什么 gcc 允许它?,是 gcc 不需要报告任何内容,因为这不是违反约束。此外,gcc 并未声称报告所有类型的未定义行为,即使使用 -Wall 或 -Wpedantic 也是如此。这是未定义的行为,这意味着实现可以选择如何处理它,而 gcc 选择在没有警告的情况下编译它(显然它只是忽略了参数)。

【讨论】:

  • 在 C89 之前的日子里,大多数 C 编译器会忽略任何未使用的参数,并允许调用者省略与未使用参数相关的参数。验证正确的调用约定通常很有用,但一些现有的 API 可能需要向函数传递不同数量和类型的参数。正常的处理方式是使用...,但... 函数的参数值与“正常”函数的参数值不同。
【解决方案2】:

出于遗留原因,使用() 为参数列表声明函数本质上意味着“在调用函数时找出参数”。要指定函数没有参数,请使用(void)

编辑:我觉得我在这个问题上因为老了而声名狼藉。让你们的孩子知道过去的编程是什么样的,这里是my first program。 (不是 C;它向您展示了我们在此之前必须使用的内容。)

【讨论】:

  • 等等,你是认真的吗?你为什么要这样做?
  • 类型规则、面向对象、封装、重载和其他现代语言特性不是一夜之间发展起来的。几十年前,编程语言更加原始。和实验性的。 C 语言比之前的语言有所进步,函数参数的强类型化的原因或您将如何实现它们尚不清楚。当时的主要动机是为程序员提供简单的方法来做强大的事情。那时,使用强类型来减少错误的需求并没有那么大的动机。
  • @Drise:你不会,不会了。但是,在 1989 年标准之前,C 语言不支持原型声明(其中声明了参数的数量和类型);您只能在声明中指定函数的返回类型。如果您在声明中更改空参数列表的规则,会有 很多 遗留代码会中断,因此它仍然受支持,但新代码应始终使用原型语法声明函数时。
  • 在类型安全方面,早期的 C 仅比 B 有所进步。有很多安全类型的语言(Simula、Pascal 甚至 Algol)在强制执行函数参数的强类型方面没有问题。 C 语言通过在资源受限的小型计算机上更高效、更容易实现而获胜,但当时所涉及的权衡是显而易见的。
  • @John Bode 关于 1989 年之前的 C 是正确的。IIRC 它也被称为传统 C。另一个很棒的“功能”允许将 int 用于 char。我的第一个 C 编译器是传统的 C,它教会我热爱 ANSI C。
【解决方案3】:
void foo() {
    printf("Hello\n");
}

foo(str);

在 C 语言中,此代码不违反约束(如果它是用 void foo(void) {/*...*/} 在其原型形式中定义的)并且由于不违反约束,编译器不需要发出诊断。

但是根据以下 C 规则,该程序具有未定义的行为:

发件人:

(C99, 6.9.1p7) "如果声明器包含一个参数类型列表,该列表还指定了所有参数的类型;这样的声明器还可以作为函数原型,供以后在同一个函数中调用同一个函数翻译单元。如果声明器包含标识符列表,142) 参数的类型应在以下声明列表中声明。"

foo 函数不提供原型。

发件人:

(C99, 6.5.2.2p6) “如果表示被调用函数的表达式的类型不包含原型 [...] 如果参数的数量不等于参数的数量,则行为是未定义。”

foo(str) 函数调用是未定义的行为。

C 不强制实现为调用未定义行为但您的程序仍然是错误程序的程序发出诊断。

【讨论】:

    【解决方案4】:

    在 C 中,使用空参数列表声明的函数在被调用时接受任意数量的参数,这些参数受制于通常的算术提升。调用者有责任确保提供的参数适用于函数的定义。

    要声明一个接受零参数的函数,您需要编写void foo(void);

    这是出于历史原因;最初,C 函数没有原型,因为 C 是从B(一种无类型语言)演变而来的。添加原型后,为了向后兼容,语言中保留了原始的无类型声明。

    要让 gcc 对空参数列表发出警告,请使用 -Wstrict-prototypes

    如果在未指定参数类型的情况下声明或定义函数时发出警告。 (如果前面有一个指定参数类型的声明,则允许在没有警告的情况下进行旧式函数定义。)

    【讨论】:

      猜你喜欢
      • 2020-08-15
      • 2020-01-16
      • 1970-01-01
      • 2013-01-27
      • 2019-11-15
      • 2021-04-13
      • 2015-09-05
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多