【问题标题】:Why does passing extra arguments to a C function does not result in a compile-time error?为什么将额外的参数传递给 C 函数不会导致编译时错误?
【发布时间】:2014-12-05 20:18:02
【问题描述】:

我可以使用 gcc 4.4.7 编译和运行以下代码。

文件:m.c

#include <stdio.h>

int main()
{
    printf("%d\n", f(1, 2, 3));
}

文件:f.c

int f(int a, int b)
{
    return a + b;
}

输出:

$ gcc m.c f.c && ./a.out
$ 3

在同一文件中定义函数f() 时,编译器会按预期抛出错误。我的猜测是编译器无法检测到编译单元之间函数的错误使用。但是链接器不应该能够检测到它吗?标准是否指定了预期的行为?

请注意,这与声明没有任何参数的函数不同,即使在单个文件中也可以。 (Why does gcc allow arguments to be passed to a function defined to be with no arguments?)。

我正在使用 gcc (GCC) 4.4.7 20120313 (Red Hat 4.4.7-11) 和 GNU ld 版本 2.20.51.0.2-5.42.el6 20100205。

【问题讨论】:

  • C 没有进行类型检查或类型安全:-(
  • 为了与 1989 年之前编写的 C 程序兼容。但是,上面的代码调用了未定义的行为。 “如果表示被调用函数的表达式的类型不包含原型......如果参数的数量不等于参数的数量,则行为未定义。”
  • 为避免此问题,请在f.c 和任何调用f 的源文件中添加带有原型int f(int a, int b); 的标头f.h#include 该标头。并使用参数调用 gcc,使其警告对未声明函数的调用,-std=cXX 用于不需要 GNU 扩展的代码,或 -std=gnuXX 用于需要 GNU 扩展的代码(其中 XX9911 )。
  • 在 C(和其他几种语言)中,传递的参数被压入堆栈,最后一个参数首先由调用函数,然后是 pc,然后是来自被调用函数的自动变量。因此,从被调用函数的角度来看,它在堆栈上看到的并没有什么区别。这种将最后一个参数首先压入堆栈的方法使 va-arg 类型的函数(如 printf)能够正常工作。
  • 额外的参数在编译时没有被捕获,因为代码缺少每个函数的原型(应该总是被使用,并且 gcc 的某些参数将强制使用)原型。

标签: c gcc compiler-errors linker


【解决方案1】:

gcc 目前默认使用-std=gnu89 编译(不确定 5.x,但之前的版本是这样)。在 C89 中(GNU89 几乎是 C89 的超集),如果在没有可见声明的情况下调用函数,则假定它被声明为

extern int f();

具有外部链接的函数,返回 int,并接受未指定(但固定)数量的默认提升参数。

这被许多人认为是设计错误,在 C89 中被标记为过时,最终在 C99 中被删除。 Gcc 默认会给出隐式函数声明的警告。

如果您调用具有错误类型或参数数量的函数,则行为未定义,仅当原型声明在调用范围内时才需要进行诊断。该标准对实现的功能没有任何要求,允许链接器失败(但通常不会)。

使用带有原型的头文件来获得警告。

通常,C 编译器将源文件单独编译成目标文件,其中存储函数的符号,而没有任何关于其参数类型的信息,因此链接器无法检查它们。

【讨论】:

  • 另外,我建议始终使用-Wall选项进行编译,它会突出这个问题以及许多其他常见错误。
【解决方案2】:

编辑:

我想我一开始误解了:

"当没有函数声明时,gcc 不知道在编译阶段会发生什么,只要它在链接阶段找到函数,一切都会工作。这个'错误'必须在编译过程中被捕获阶段,因为从技术上讲,它在链接阶段根本不是错误。”

旧答案:

这是一项功能,而不是错误。当你调用一个函数时,参数被压入堆栈。如果你不使用它们,一般没什么大不了的。

您甚至可以计划使用未知数量的参数。这是用于记录的自定义 printf 样式函数的简单示例:

void Debug_Message(uint32_t level, const char *format, ...)
{
    char buffer[256];

    //check level and do stuff

    va_list args;
    va_start(args, format);
    vsnprintf(buffer, sizeof(buffer), format, args);

   //buffer now contains data as if we did an sprintf to it
}

这会像 printf 一样被调用,可能是:

Debug_Message(1, "%d%d%d", 1, 2, 3);

可能是:

Debug_Message(1, "%d", 1);

【讨论】:

  • 您正在描述(无害的)运行时效果。 OP 询问为什么没有 compile-time 错误。
  • 哎呀,在这种情况下我不得不说这是因为没有函数声明。当没有函数声明时,gcc 不知道在编译阶段会发生什么,只要它在链接阶段找到函数,一切都会正常工作。这个“错误”必须在编译阶段被捕获,因为它在技术上根本不是链接阶段的错误。 @barakmanos
  • 这也是我的想法,但据我所知,当没有声明时,编译器会隐式假定int f()int f(int),它们都不匹配对f(1, 2, 3) 的调用.所以我仍然希望出现编译错误(或警告)。话虽如此,我不确定标准“对此有什么看法”。无论如何,如果您认为这是正确的答案,那么也许您应该相应地更新您的答案。
  • 如果编译器将类型信息发送到目标文件中,链接器可能会检测到f 没有匹配的定义。 C 对象文件格式是否包含函数参数的类型信息?
  • 就链接器而言,我认为定义并不重要。进入函数时压入的堆栈只是一个字节块,定义定义了哪些字节用于哪个变量。这就是为什么有 '...' 参数定义告诉编译器我们不知道会有多少变量。我相信您定义的严格定义仅仅是帮助防止错误和在堆栈中自动分配变量的指南。 @Sourav
猜你喜欢
  • 1970-01-01
  • 2017-06-24
  • 2013-04-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-01-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多