C/C++原型的目的

27
我在阅读C/C++原型语句的维基百科页面时感到困惑:
维基百科说:“通过包含函数原型,您通知编译器函数"fac"需要一个整数参数,并使编译器能够捕获这些类型的错误。”
并使用以下内容作为示例:
#include <stdio.h>

 /* 
  * If this prototype is provided, the compiler will catch the error 
  * in main(). If it is omitted, then the error will go unnoticed.
  */
 int fac(int n);              /* Prototype */

 int main(void) {             /* Calling function */
     printf("%d\n", fac());   /* ERROR: fac is missing an argument! */
     return 0;
 }

 int fac(int n) {             /* Called function  */
     if (n == 0) 
         return 1;
     else 
         return n * fac(n - 1);
}

但调用函数的函数定义已经包含了原型告诉编译器的所有信息,为什么编译器不能从被调用函数的定义中推导出这些信息,因为它们包含完全相同的语句/信息?

我缺少什么?看起来是徒劳无功的额外工作。

编辑:谢谢各位。我想当然地认为编译器是多次通过的。我对当前像Python这样的语言很宠爱。由于它太旧而需要一些修补才能在单个通道中准确执行某些操作。现在对我来说更明显了。显然,这需要对编译器链接和编译的方式有相当深入的了解。


8
请注意,这篇维基百科文章中有错误的内容。你可以在讨论页面上看到一些人不愿意让它被更正。我已经放弃了更正它。 - Johannes Schaub - litb
这就是为什么我在调用函数之前定义它们(无论是在同一个源文件中)。 这消除了单独声明的需要,尽管这意味着我的代码阅读起来“反向”。 - John Bode
6个回答

27

两个原因:

  1. 编译器从上到下读取文件。如果facmain中使用,而且没有原型存在,则编译器不知道如何检查该调用是否正确,因为它还没有到达fac的定义。

  2. 可以将C或C ++程序拆分成多个文件。fac可能在与编译器当前处理的文件完全不同的文件中定义,因此它需要知道该函数某处存在以及如何调用它。

请注意,您发布的示例中的注释仅适用于C。在C ++中,即使省略了原型(尽管会产生不同的错误,具体取决于原型是否存在),该示例也将始终产生错误。在C ++中,要求在使用之前定义或设置所有函数。

在C中,您可以省略原型,并且编译器将允许您以任意数量的参数(包括零)调用该函数,并假定返回类型为int。但是,仅因为它在编译期间没有警告,就不意味着如果您不以正确的方式调用函数,程序将正常工作。这就是在C中使用原型很有用的原因:因此编译器可以代表您进行双重检查。

C和C ++背后的哲学是这种功能的动机,它们是相对较低级别的语言。它们不进行大量手把手教学,也不进行任何运行时检查。如果您的程序执行了错误操作,则会崩溃或表现出奇怪的行为。因此,这些语言包括此类特征,以使编译器能够在编译时识别某些类型的错误,以便您可以更轻松地找到并修复它们。


在第二种情况下,你认为包含什么?它们包含原型!但是如果你正在编写自己的 fac 函数,需要提供原型,无论是在单独的包含文件中还是在同一源文件中。在第一种情况下(我假设你指的是 Mystagogue 的答案),问题不在于递归依赖有什么问题,而在于如果你确实有递归依赖,就没有办法对这两个函数进行排序,以便编译器在处理调用任何一个函数之前读取两个函数的定义。 - Tyler McHenry
基本上,你所有的问题都需要理解 C/C++ 编译器/链接器以及 C/C++ 如何在项目中编译和链接多个 .cpp.c 文件。#include 只包含头文件,通常只包含原型。实际函数可能是在另一个 .cpp 文件或 .c 文件中编写的。为了让编译器意识到“好的,这是一个真正的函数而不是语法错误”,它需要在头文件中有一个原型来告诉它。 - Charles Salvia
2
@pythonnewbie:现在这些问题(以及许多其他问题)都很容易解决,但请记住,C语言可以追溯到70年代。当然,该语言本来可以要求使用多遍编译器,以便访问所有已使用的.cpp文件(而不仅仅是具有原型的头文件和已编译的对象文件),以便推断函数签名和其他内容。但那时候这样做非常昂贵,所以他们没有这样做。 - user395760
@pythonnewbie:有些人可能会说“优雅” ;) - caf
实际上,整个设计是多遍的,从某种意义上说:预处理器遍和链接器遍是分开的。记住,在过去,你会有64 KB的问题。逐个编译源文件可以保持所需的RAM数量较低。唯一看到所有部分的组件是链接器,而C语言被设计为与愚蠢(=内存高效)的链接器一起工作。 - MSalters
显示剩余4条评论

15

原型能够将界面与实现分离。

在您的示例中,所有代码都位于一个文件中,您可以很容易地将fac()定义移到当前原型处并删除原型。

现实世界的程序由多个 .cpp 文件 (也称为编译单元) 组成,通常在链接成库之前编译和链接,然后再链接成最终可执行形式。对于这种性质的大型项目,原型被收集到 .h 文件 (也称为头文件) 中,在编译时将该头文件包含在其他编译单元中以通知编译器库中功能的存在和调用约定。在这些情况下,函数定义不可供编译器使用,因此原型 (也称为声明) 作为一种合同,定义了库的功能和需求。


7
原型的最重要原因是解决循环依赖问题。如果“main”可以调用“fac”,而“fac”又调用“main”,那么您将需要一个原型来解决这个问题。

这两个原因是声明必要的:一是与已编译库进行链接,另一个是为了让代码更美观和符合个人喜好。其他所有原因都只是次要的。 - codechimp

4

C和C++是两种不同的语言,在这个特定的情况下,两者之间有很大的区别。从问题的内容来看,我认为你在谈论C语言。

#include <stdio.h>
int main() {
   print( 5, "hi" );  // [1]
}
int print( int count, const char* txt ) {
   int i;
   for ( i = 0; i < count; ++i ) 
      printf( "%s\n", txt );
}

这是一个合适的C程序,它可以按照预期打印5行“hi”。C语言在[1]处找到调用,它假定print是一个返回int并且需要未知数量参数(编译器不知道,但程序员知道)的函数,编译器假设调用是正确的并继续编译。由于函数定义和调用匹配,程序是良好形式的。
问题在于当编译器解析[1]行时,它不能执行任何类型检查,因为它不知道这个函数是什么。如果我们在写这行代码时弄错了参数顺序,例如输入print( "hi", 5 );,编译器仍然会接受这行代码,因为它没有关于print的先前知识。尽管代码编译成功,但由于调用不正确,它将在后面失败。
通过提前声明函数,您向编译器提供了所需的信息以在调用位置进行检查。如果声明存在,并且发生相同的错误,编译器将检测到错误并通知您。
另一方面,在C++中,编译器不会假定调用是正确的,实际上它要求您在调用之前提供函数声明。

3

C编译器按照自上而下的顺序处理源文件。在解析参数类型时,不会考虑出现在其使用之后的函数。因此,在您的例子中,如果main()在文件底部,则无需为fac()提供原型(因为编译main()时已经看到了fac()的定义)。


1
除了已经给出的所有好答案之外,还要考虑一下:如果你是一个编译器,你的工作是将源代码翻译成机器语言,而你(作为尽职的编译器)只能逐行读取源代码--如果没有原型,你会如何阅读你粘贴的代码?你怎么知道函数调用是有效的而不是语法错误?(是的,你可以做个笔记,在最后检查是否匹配,但那是另一回事。)
另一种看待它的方式(这次是作为人类):假设你没有将函数定义为原型,也没有其源代码可用。然而,你知道在你的伙伴给你的库中有机器代码,当运行时,返回某个预期的行为。多好啊。现在,如果没有原型告诉它“嘿,伙计们相信我,有一个名为这样和这样的函数,需要参数并返回某些东西”,你的编译器怎么知道这样的函数调用是有效的呢?
我知道这是一种非常、非常、非常简单化的思考方式。给软件添加意图可能是一个坏迹象,不是吗?

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