C89、C90或C99版本中是否要求所有函数都有原型?

55

为了真正符合标准,C语言中的所有函数(除了main函数)是否都必须有原型,即使它们只在同一翻译单元中定义后使用?


1
这个问题 在C语言中必须声明函数原型吗? 曾经被提出作为这个问题的一个重复。关闭一个较旧的问题作为一个较新的问题的重复,而不是反过来,必须有一个强有力的理由。 - Jonathan Leffler
6个回答

43
取决于您所说的“真正符合标准”的含义。然而,简短的答案是“在使用函数之前,确保所有函数都有作用域原型是一个好主意”。更合格的答案指出,如果函数接受可变参数(特别是printf()系列函数),则必须有原型在作用域内才能严格符合标准。这适用于C89(来自ANSI)和C90(来自ISO;除了章节编号,与C89相同)。但除了'varargs'函数之外,返回int的函数不必声明,返回其他类型的函数需要显示返回类型的声明,但不需要参数列表的原型。

然而,请注意,如果函数接受的参数在没有原型的情况下受到“正常提升”的影响(例如,接受charshort的函数 - 这两个都转换为int;或者接受float而不是double的函数),则需要原型。标准对此比较宽松,以允许旧C代码在符合标准的编译器下编译;旧代码不需要担心在使用之前声明函数 - 定义上,旧代码不使用原型,因为在有标准之前,原型不可用于C。

C99禁止“implicit int”...这意味着奇怪的情况,比如“static a;”(默认为int)和隐式函数声明。这些被提到(连同其他大约50个主要更改)在ISO/IEC 9899:1999的前言中,该标准将其与以前的版本进行了比较:

  • 删除implicit int
  • 删除implicit function declaration

在ISO/IEC 9899:1990中,§6.3.2.2 Function calls指出:

如果函数调用中括号中的参数列表之前的表达式仅由标识符组成,并且对于此标识符没有可见的声明,则该标识符会被隐式声明,就好像在包含函数调用的最内层块中声明了:

extern int identifier();

出现了38

38这意味着一个具有外部链接的块作用域标识符,其类型为没有参数信息且返回int的函数。如果实际上它未被定义为具有“返回int的函数”类型,则行为是未定义的。

这段话在1999年标准中缺失。我还没有追踪到措辞上的变化,使得C90允许使用static a;而C99则不允许(需要static int a;)。

请注意,如果函数是静态的,可以在使用之前定义,并且不必在前面加上声明。GCC可以通过指定选项-Wmissing-prototypes来发出警告,如果没有在使用之前声明非静态函数。


3
对于幽默地使用“witter”(长篇大论地谈论琐碎的主题)给予加分。我会扣分因为常见的误用“verbiage”(过度冗长的措辞)来表示“语言”,但在考虑了C标准的文本后,我决定将其视为更微妙和非常精准的幽默。 - Jeff Learman

21
一个原型(prototype)是一个函数声明,它指定了函数参数的类型。
在 ANSI C 标准之前(即 Kernighan & Ritchie 的《The C Programming Language》第一版描述的语言),没有原型;函数声明无法描述参数的数量或类型,调用者需要传递正确的参数数量和类型。
ANSI C 引入了原型,即指定参数类型的声明(这是从早期的 C++ 借鉴来的特性)。
在 C89/C90 的标准下(ANSI 和 ISO 描述相同的语言),可以使用没有可见声明的方式来调用函数,会提供隐式声明。如果隐式声明与实际定义不兼容(例如调用sqrt("foo")),则行为未定义。既不是原型声明也不是非原型声明可以与可变参数函数兼容,因此对可变参数函数(如printf或scanf)的任何调用必须有可见的原型。
C99 废除了隐式声明。对于没有可见声明的函数调用是一个约束违规,需要编译器进行诊断。但仍然不需要该声明是原型,它可以是旧式声明(不指定参数类型)。
C11 在这个领域没有做出重大改变。
因此,即使在 2011 年的 ISO C 标准下,符合要求的代码仍然允许使用旧式函数声明和定义(自 1989 年以来已经被“废弃”)。对于所有版本的 C 语言,为所有函数使用原型是非常合理的。旧式声明和定义只是为了避免破坏旧代码。

2
@supercat:不正确。如果非变参函数的定义声明了,比如说,2个参数,那么调用不传递恰好2个适当类型的参数将具有未定义行为。使用非原型声明只是防止编译器诊断错误。 - Keith Thompson
1
在标准出现之前,任何未来需要支持那个预先存在的代码的平台实现都将被迫支持可变参数调用,无论标准是否要求。 - supercat
1
标准非常明确地不支持你所谈论的内容。N1570 6.5.2.2 第6段:“如果参数数量与参数个数不相等,则其行为是未定义的。”你所谈论的现有代码正是为什么<stdarg.h>和显式可变参数函数被引入的原因之一。你所提到的一个例子是 POSIX 的 open() 函数,它传统上需要 2 或 3 个参数; POSIX 将其指定为可变参数函数。问题是关于 C89/C90 和 C99,而不是 ANSI 之前的 C。 - Keith Thompson
2
如果你要倡导像这样极其不可移植的做法,至少要非常清楚地指出它们是不可移植的,并且你是基于一个42年前的文档做出假设的,而这个文档已经被多次取代。向函数传递错误数量的参数是不可移植的,而且也不是常见的做法。 - Keith Thompson
你知道有哪些平台或C11实现,一个本来有效的printf()调用由于缺少原型而失败吗?换句话说,“必须有可见的原型”是什么意思?(int main(void){int i = 1; printf("%d", i); }) - jfs
显示剩余9条评论

15
不,函数并不总是需要原型。唯一的要求是在使用函数之前“声明”它。有两种声明函数的方式:编写原型或编写函数本身(称为“定义”)。定义始终是一个声明,但并非所有声明都是定义。

13
在 C99 中,你是正确的。在 C89/C90 中,你不需要预先声明函数;只要将其用作函数,它就会被隐式声明为接受未定义参数列表并返回 int 的函数。 - Jonathan Leffler
5
C99和早期C标准之间的区别可能是显著的,正如在这个comp.lang.c FAQ问题中所证明的:http://c-faq.com/malloc/mallocnocast.html。 - nagul
虽然你的回答很好,但是你可能需要注意一些编译器会假设调用一个未声明的函数时,它是一个int函数,并且其参数与调用中传递的参数完全匹配,考虑到标准类型提升。这样的编译器通常会在同一编译单元中找到与推断不一致的声明时报错。如果没有找到声明,并且参数类型与分开编译的函数定义没有正确猜测(进行比较),问题可能会在链接时被检测出来,也可能不会。 - supercat
1
声明 "int foo();" 不是原型,但足以允许代码调用 "foo" 并传递任意数量的参数,前提是 "foo" 在某个地方使用了 "旧" 风格定义,并且它从不尝试使用比传递给它的参数更多的参数。 - supercat
@supercat:抱歉我没有早些回复这个评论。这是不正确的。如果使用与其定义不一致的参数调用foo,则行为是未定义的。例如,如果foo被定义为具有2个int参数,则使用3个foo参数调用它会产生未定义的行为。无论您尝试使用这种非可移植性的黑客技巧做什么,都有更好且更可移植的方法来实现它。 - Keith Thompson
@KeithThompson:标准编写时最常用的调用约定是明确设计为允许这种用法的。在C语言变得流行之前,Macintosh和PC平台上的普通调用约定不允许这种用法,但是为这些平台设计C编译器的人选择使用可以支持它的调用约定,而不是使用平台的正常约定。标准并不要求实现支持这种用法,但编写Mac和PC编译器的人似乎认为优质的实现应该这样做。 - supercat

3
写新函数时的一个好方法是将它们倒过来写,将主要部分放在底部,这样当您改变函数的参数或返回类型时,就不必修复原型。不断修复原型,并处理所有编译器过期警告会变得非常繁琐。
一旦您的函数顺畅地一起工作,请将代码移动到一个命名良好的模块中,并将原型放入同名的.h文件中。这可以节省大量时间。这是我在5年中发现的最大的生产力助手。

1
是的,每个函数都必须有一个原型,但该原型可以出现在单独的声明中,也可以作为函数定义的一部分。在使用 C89 及以上版本编写的函数定义中,通常都会包含原型,但如果按照经典的 K&R 格式编写代码,则需要手动提供原型:
main (argc, argv)

  int argc;
  char **argv;

{
  ...
}

那么函数定义就没有原型。如果您使用 ANSI C(C89)风格编写,如下所示:

main (int argc, char **argv) { ... }

那么函数定义就有原型。


2
K&R函数定义在C89中仍然是合法的(尽管不建议使用),因此“每个函数必须有原型”这一说法并不正确。 - M.M
这个答案自相矛盾,但在展示K&R C风格的函数定义中定义函数参数方面非常有用。希望我们永远不再看到这样的代码,但有时我们确实需要进行一些代码考古! - Jeff Learman
1
@JeffLearman:它可能有用,但事实上是错误的。 - Keith Thompson
@KeithThompson 是的,这显著地削弱了它的有用性。否则的话,我本来会点赞它的。 - Jeff Learman

0
据我所知(在ANSI C89 / ISO C90中),不行。 我对C99不确定; 但是,我期望相同。
个人笔记:只有在以下情况下才编写函数原型...
1.当我需要时(当A()调用B()并且B()调用A()),或者 2.我正在导出该函数; 否则,它就感觉多余。

原型是指定参数类型的函数声明。它不需要是单独的声明,可以是函数定义的一部分。例如,这个定义:void func(int n) { /* ... */ } 包括一个原型。 - Keith Thompson
@KeithThompson 对,但我认为他的意思是“只有当……时我才编写单独的函数原型”。这是非常普遍的做法。除非我们在头文件中对它们进行原型设计,否则将所有函数都设置为静态函数也是一种好的做法。感谢编译器警告调用没有原型的函数!正如我们这些在80年代编写代码的人所知道的那样,这减轻了C代码中最常见的错误原因。 - Jeff Learman
@JeffLearman 我不喜欢对人们的意思进行隐含的假设。你的解释很可能是正确的,但这种措辞在大多数情况下也与使用旧式声明和定义以及在所列情况下使用原型相一致。 - Keith Thompson

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