我正在阅读关于C / C ++ Prototype语句的维基百科,我很困惑:
维基百科说:“通过包含函数原型,您可以通知编译器函数”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);
}
但被调用函数的函数定义已经包含 all 原型告诉编译器的信息,为什么编译器不能从被调用函数的定义中推导出这些信息,因为它们包含相同的声明/信函?
我错过了什么?似乎是额外的工作,没有明显的收获。
编辑:谢谢你们。我猜想编译器是多遍的。我被宠坏了像Python这样的当前语言。这是有道理的,因为它需要一些kludges才能在一次通过中准确地完成任务。现在对我来说似乎更加明显。显然,它需要对编译器如何链接和编译有相当深入的了解。
答案 0 :(得分:25)
有两个原因:
编译器从上到下读取文件。如果fac
中使用的main
高于fac
且没有原型,则编译器不知道如何检查该调用是否正确完成,因为它没有但达到了fac
。
可以将C或C ++程序拆分为多个文件。 fac
可以在与编译器当前正在处理的文件完全不同的文件中定义,因此它需要知道该函数存在于某个地方,以及它应该如何被调用。
请注意,您发布的示例中的注释仅适用于C.在C ++中,该示例将始终产生错误,即使原型被省略(尽管它会产生不同的错误,具体取决于关于原型是否存在)。在C ++中,所有函数都需要在使用之前定义或原型化。
在C中,您可以省略原型,编译器将允许您使用任意数量的参数(包括零)调用函数,并假设返回类型为int
。但仅仅因为它在编译期间没有对你大喊大叫并不意味着如果你不以正确的方式调用函数,程序将正常工作。这就是为什么它在C中原型很有用:所以编译器可以代表你仔细检查。
推动这种功能的C和C ++背后的哲学是这些是相对低级的语言。他们不会进行大量的手持操作,如果进行任何运行时检查,他们也不会做太多。如果你的程序做错了什么,它会崩溃或表现得很奇怪。因此,这些语言包含这样的功能,使编译器能够在编译时识别某些类型的错误,以便您可以更轻松地找到并修复它们。
答案 1 :(得分:15)
Prototypes允许您将接口与实现分开。
在您的示例中,所有代码都存在于一个文件中,您可以轻松地将fac()定义移动到当前原型的位置并移除原型。
真实世界的程序由多个.cpp文件(也称为编译单元)组成,经常编译并链接到库中,然后链接到最终的可执行形式。对于那种性质的大型项目,原型被收集到.h文件(也称为头文件)中,其中头在编译时包含在其他编译单元中,以警告编译器存在和调用库中的功能约定。在这些情况下,函数定义不可用于编译器,因此原型(也称为声明)用作定义库的功能和要求的一种契约。
答案 2 :(得分:5)
原型的最重要原因是解决循环依赖。如果“main”可以调用“fac”,“fac”调用“main”,那么你需要一个原型来解决它。
答案 3 :(得分: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
的函数,并且获取了未知数量的参数(编译器不知道,程序员已知),编译器< em>假设呼叫正确并继续编译。由于函数定义和调用匹配,程序形成良好。
问题是当编译器解析[1]处的行时,它不能执行任何类型的类型检查,因为它不知道函数是什么。如果我们写这行时我们误认为参数的顺序,我们输入print( "hi", 5 );
编译器仍会接受该行,因为它没有print
的先验知识。由于调用不正确,即使代码编译它也会在以后失败。
通过预先声明函数,您可以为编译器提供在调用地点检查所需的信息。如果声明存在,并且出现同样的错误,编译器将检测错误并通知您错误。
另一方面,在C ++中,编译器不会假设调用是正确的,并且实际上要求在调用之前提供函数声明。
答案 4 :(得分:3)
C编译器从上到下处理源文件。在解析参数类型时,不会考虑在使用之后出现的函数。因此,在您的示例中,如果main()
位于文件的底部,那么您将不需要fac()
的原型(因为编译器已经看到fac()
的定义在编译main()
时)。
答案 5 :(得分:1)
除了已经给出的所有好答案之外,请考虑一下:如果您是编译器,并且您的工作是使用机器语言翻译源代码,而您(作为尽职尽责的编译器)只能读取源代码逐行 - 如果没有原型,你会如何阅读你粘贴的代码?你怎么知道函数调用是有效的而不是语法错误? (是的,如果一切都匹配,你可以做一个笔记并在最后检查,但这是另一个故事)。
另一种看待它的方式(这次是作为一个人):假设你不将函数定义为原型,也是它的源代码可用。但是,您知道,在您的配合提供给您的库中,有一些机器代码在运行时返回某个预期的行为。多好。现在,如果没有原型告诉它,你会怎么编译知道这样的函数调用是有效的“嘿伙计信任我,有一个名为such的函数,这样接受参数并返回一些东西”?
我知道这是一种非常非常非常简单的思考方式。为软件添加意向性可能是一个不好的迹象,不是吗?