在C语言中必须声明函数原型吗?

64

我有一些Java、C#和C++的经验,但对于C语言还比较新。在C语言中,声明函数原型是否必要?如果不进行声明,代码是否能够编译通过?在编程实践中,声明函数原型是好的习惯吗?还是这取决于编译器?(我正在运行Ubuntu 9.10,并在Code::Blocks IDE下使用GNU C编译器gcc)


10个回答

74
在 C 语言中,不论是旧的版本(包括 C89/90)还是新的版本(C99),都没有必要为函数声明一个原型。然而,在 C89/90 和 C99 中,函数声明存在显著区别。
在 C89/90 中,根本不需要声明函数。如果在调用点未声明函数,则编译器会从调用时传递的参数类型中“猜测”(推断)函数声明,并假定返回类型为 int。
例如:
int main() {
  int i = foo(5); 
  /* No declaration for `foo`, no prototype for `foo`.
     Will work in C89/90. Assumes `int foo(int)` */

  return 0;
}

int foo(int i) {
  return i;
}

C99规定,在调用函数之前必须先进行声明。然而,这并不一定要用原型进行声明。非原型的声明同样有效。这意味着在C99中,“隐式 int” 规则不再适用于函数的推断返回类型(在这种情况下),但如果函数没有使用原型声明,参数类型仍然可以从参数类型进行猜测。

上述示例代码在C99中将无法编译,因为在调用处未声明foo函数。但是,你可以添加一个非原型的声明。

int foo(); /* Declares `foo`, but still no prototype */

int main() {
  int i = foo(5); 
  /* No prototype for `foo`, although return type is known. 
     Will work in C99. Assumes `int foo(int)` */

  return 0;
}
...

最终将得到有效的C99代码。

然而,在调用函数之前声明函数原型始终是一个好习惯。

另外需要注意的是:我上面说过,从来不需要声明函数原型。实际上,对于某些函数,这是必需的。为了正确调用C语言中的可变参数函数(例如printf),必须在调用点之前声明该函数并带有原型。否则,行为是未定义的。这适用于C89 / 90和C99。


7
对于一个详细回答并解释得很好的答案,给予+1。 - legends2k
“在C99中无法编译”——在我的系统上使用-std=c99选项,gcc和clang只会出现警告而不是编译错误。ideone也可以编译它 - jfs
6
GCC文档提供帮助:「(...)为了获取符合标准所需的所有诊断信息,您还应指定 -pedantic(或者如果您想将它们变成错误而不是警告,则使用 -pedantic-errors)(...)」。 - Przemek
@Przemek你想要表达什么?你是在说"-std=c99"不足以宣告它可以“编译为c99”吗?也许你是对的,这取决于某个人确切地想要表达什么,而你的解释可能更接近答案的作者所指的“无法在C99中编译”。 - jfs
1
@jfs,因为在GCC中,“-std=c99”表示c99标准加上gnu扩展。问题可能会编译通过,因为GNU扩展允许它。你需要添加“-Wpedantic”以便GCC发出所有严格ISO C要求的警告。 - MaxPlankton
-pedantic选项会导致gcc打印违反约束和语法规则所需的诊断信息。在某些情况下,这些诊断信息仅仅是警告——并且很难区分这些警告和语言不要求的其他警告之间的区别。将-pedantic替换为-pedantic-errors,可以使gcc将语言违规视为致命错误。” - Rick

71
在 ANSI C 中(指 C89 或 C90),你不必声明函数原型,但最好使用它们。标准之所以允许你不使用它们,是为了向后兼容非常古老的代码。
如果你没有原型,并且调用一个函数,编译器将从你传递给函数的参数中推断出原型。如果在同一编译单元中稍后声明该函数,则如果函数的签名与编译器猜测的不同,你将会得到编译错误。
更糟糕的是,如果该函数在另一个编译单元中,则无法获得编译错误,因为没有原型来检查。在这种情况下,如果编译器猜错,当函数调用将不同类型的值压入堆栈时,你可能会得到未定义行为。
惯例是在与包含函数的源文件相同名称的头文件中始终声明原型。
在 C99 或 C11 中,标准 C 要求在调用任何函数之前在作用域内声明函数。实际上,除非强制要求,否则许多编译器不会执行此限制。

1
如果实际函数类型与推断的类型不同,您不一定会得到“编译错误”。这在C语言中是未定义的行为,但不是约束违规。 - AnT stands with Russia
1
你说得对,我应该更具体一些,我的意思是大多数编译器会抛出一个警告。 - user308405
2
在C99中,您确实需要一个函数声明 - 尽管我认为它不一定是原型声明。 C90(C89)对此更加宽松。请注意,如果函数使用“可变参数”,则需要在作用域中有原型 - 即使在C89 / C90中也是如此。 - Jonathan Leffler
2
@JeremyP:编译器默认情况下仍然(太)宽容,这主要是因为仍需要更新的遗留代码库。除非你强制执行标准(例如 -Werror,也许还有 -pedantic-Wold-style-definition-Wold-style-declaration-Wmissing-prototypes-Wstrict-prototypes 或以上所有(或大部分)),否则编译器不会强制执行标准。我通常使用 -std=c11 和所有上述选项,但我确保 POSIX 或相关的 #define 处于活动状态。我很少编写 #define _GNU_SOURCE#define _BSD_SOURCE - Jonathan Leffler
1
@JonathanLeffler:我见过一些系统在非原型函数和带参数的原型函数上使用不同的默认调用约定,而且还使用不同的链接时命名。如果一个函数有原型并且没有标记为使用基于堆栈的参数,则尝试在没有可见声明的情况下调用它将导致链接时错误。 - supercat
显示剩余5条评论

6

如果函数在使用之前已经被定义,那么这并不是必须的。


在使用之后定义它的意义何在? - endolith
1
代码文件中只需按逻辑顺序编写代码,或将函数定义在其他文件中即可。 - Drakosha
事实上,函数的新式定义既是原型也是声明。如果定义在任何使用之前发生,那么另一个没有主体的原型将是无意义的。 - Przemek

4

虽然不是必需的,但不使用原型是一种不好的实践。

使用原型可以让编译器验证您是否正确调用函数(使用正确数量和类型的参数)。

如果没有原型,则可能会出现这种情况:

// file1.c
void doit(double d)
{
    ....
}

int sum(int a, int b, int c)
{
    return a + b + c;
}

并且这个:

// file2.c

// In C, this is just a declaration and not a prototype
void doit();
int sum();

int main(int argc, char *argv[])
{
    char idea[] = "use prototypes!";

    // without the prototype, the compiler will pass a char *
    // to a function that expects a double
    doit(idea);

    // and here without a prototype the compiler allows you to
    // call a function that is expecting three argument with just
    // one argument (in the calling function, args b and c will be
    // random junk)
    return sum(argc);
}

@NeilButterworth:在编译file2.c时,doit的定义(位于file1.c中)不可见。 - sepp2k
@NeilButterworth - 我会重新格式化,但是注释// file1.c// file2.c的意思是这些是不同的文件。当编译file2.c时,编译器将没有看到file1.c中的定义。 - R Samuel Klatchko
我的评论在你的编辑之前。我会删除它。 - anon

2
在C语言中,如果我们没有声明函数原型并使用函数定义,如果函数的返回类型是“整数”,则程序可以编译并生成输出,否则将出现编译器错误。原因是,如果我们调用一个函数而没有声明函数原型,那么编译器会生成一个返回整数的原型,并搜索相似的函数定义。如果函数原型匹配,则编译成功。如果返回类型不是整数,则函数原型不匹配并生成错误。因此最好在头文件中声明函数原型。

1

C语言允许调用尚未声明的函数,但我强烈建议在使用函数之前先声明所有函数的原型,以便编译器在您使用错误参数时进行提示。


1

你应该将函数声明放在头文件(X.h)中,将定义放在源文件(X.c)中。然后其他文件可以#include "X.h"并调用该函数。


为什么是-1?这才是你应该做的。 - cpalmer
有人能解释一下为什么这是个坏主意吗?我一直以为这是标准做法。唯一的问题是循环包含。但是,如果你计划好你的包含,就不会遇到那个问题。 - Natalie Adams
2
因为并非所有功能都是公开的。如果你在 static int bar() 之后定义了 static void foo(),并且 bar 调用了 foo.. 呵呵 :) 为什么要给那些没有在公共头文件中公开的函数制作原型呢?虽然我不是那个给它点踩的人。 - Tim Post
1
我猜测这个被踩的(不是我)是因为它没有回答问题。 - anon
@Tim,bar可以调用foo,因为编译器已经从原型中知道了它。除非在C中不是这样,否则在C++中应该是这样的。你能发一个例子吗?这样我们可能会更好地理解你的意思。 @Neil 即使它没有直接回答问题,但它确实与问题相关的“最佳实践”,我认为没有必要将其投票降低。也许不是赞成票,但它并不是我想要删除或不必要的东西。 - Natalie Adams
@Tim 也许是因为程序员希望在头文件中以描述性的方式获取 c 文件中的所有函数? - Pithikos

1

根据 C99 标准,函数原型不是强制性的。


0

对于调用代码编译而言,声明函数并非绝对必要的。但是有一些注意事项需要注意。未声明的函数被假定返回int类型,并且编译器会首先发出有关函数未声明的警告,然后再针对返回类型和参数类型不匹配进行警告。

尽管如此,正确声明带有原型函数的做法显然更好。


-4
选择“选项”菜单,然后选择“编译器 | C++ 选项”。在弹出的对话框中,在“使用 C++ 编译器”选项中选择“CPP always”。 再次选择“选项”菜单,然后选择“环境 | 编辑器”。确保默认扩展名为“C”,而不是“CPP”。

1
这似乎并没有回答所提出的问题,而是关于特定IDE的一些松散相关的问题。 - Jonathan Leffler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接