函数原型的作用是什么?

5

我正在跟随一份教程学习curses,其中所有的C代码都是在main()之前原型函数内部定义,然后再进行定义。在我的C++学习中,我听说过函数原型,但从未尝试过,并且据我所知,它对代码编译的影响不大。这更多是程序员个人选择吗?如果是,那么为什么它被包含在C中呢?


4
当两个函数相互调用时,您将需要前置声明。 - kennytm
1
可能是重复问题:C89、C90或C99中是否需要所有函数的原型? - Jens Gustedt
1
我不明白为什么。我不是在问它们是否必要,我只是不理解它们为什么首先出现在标准中。 - Maulrus
简短回答是:编写编译器已经很困难了,要求事先声明所有内容是保持编译器单遍的有效方式。 - user395760
1
为什么人们将这个问题关闭为完全重复?另一个问题是在询问是否需要使用它。而这个问题是在询问为什么要使用它。仅仅因为它们的标题都有原型这个词并不意味着它们是同一个问题。 - Maulrus
5个回答

12

在C语言中,最初并未包括函数原型。当您调用函数时,编译器只是相信该函数存在,并接受您提供的参数类型。如果您获取参数的顺序、数量或类型有误,则很遗憾——您的代码将在运行时以可能难以理解的方式失败。

后来的C语言版本添加了函数原型以解决这些问题。在某些情况下,您的参数会被隐式转换为声明类型,或者作为与原型不兼容的标记,并且编译器可以标记类型顺序和数量错误为错误。这具有启用可变参数函数及其所需特殊参数处理的副作用。

请注意,在C语言中(与C ++不同),声明为foo_t func()的函数与声明为foo_t func(void)的函数不同。后者被原型化为没有参数的函数。前者声明了一个没有原型的函数。


最后一节关于声明和原型之间的区别加1分。 - legends2k

3
在C语言中,需要进行原型设计,以便在您未定义函数x()时,程序知道该函数的存在。这样,y()就知道有一个x()函数存在。由于C语言采用自上而下的编译方式,因此需要事先定义函数。
x();
y();
main(){

}

y(){
x();
}

x(){
...
more code ...
maybe even y();
}

1
我认为这个答案完全没有抓住重点。首先,您可以在作用域中没有原型的情况下调用任何函数,并在定义之前调用它(适用某些规则)。其次,“自上而下编译”(无论确切含义是什么)不是 C 的一部分,也没有要求以某种特定方式编译 C。 - Jens
第三,你的代码片段甚至没有包含适当的原型!它太懒了,gcc -Wall -pedantic -std=c89 x.c 会产生10个警告。 - Jens

1
我认为这是为了让客户能够访问库的.h文件并查看可用的函数,而无需查看实现(该实现将在另一个文件中)。
这对于查看函数返回什么/有哪些参数很有用。

1
从头文件中获取可用函数是一个不好的想法,因为头文件可能包含应用程序员无法使用的任意魔法。一个严肃的开发者将从所提供的库文档中获取所有所需信息。对于标准C、POSIX和许多其他库(甚至是Windows API),文档都是免费提供的,可能已经存在(man页面、Windows帮助等)。如果您需要从头文件中获取原型,那么您就走错了路。 - Jens

1

函数原型是编译器编写早期的遗留物。过去,编译器需要对源文件进行多次扫描以进行编译被认为效率极低。

在C语言中,在某些情况下,以一种方式引用函数在语法上等同于引用变量:考虑获取指向函数的指针与获取指向变量的指针。在编译器的中间表示中,这两者在语义上是不同的,但从上下文中无法确定标识符是变量、函数名还是无效的标识符。

由于从上下文中无法确定,没有函数原型,每次编译一个源文件时,编译器都需要对其进行额外的扫描。这将为任何编译添加额外的O(n)因素(即,如果编译是O(m),现在将是O(m*n)),其中n是项目中文件的数量。在大型项目中,编译已经需要数小时,拥有双通道编译器是非常不可取的。

提前声明所有函数将允许编译器在扫描文件时构建函数表,并能够确定它遇到的标识符是引用函数还是变量。

由于这个原因,C(以及C++)编译器在编译方面非常高效。

这并不一定是正确的 - 如果他们不需要函数原型,他们就不必重复解析相同的 #include 三十次。这就是为什么预编译头文件在 MSVC 中能够有效地减少构建时间的原因 - 因为 #include 需要很长时间。 - Puppy
3
我喜欢这个开端。是的,编译模型应该受到责备。但我不太喜欢为这个模型辩护。建议C++编译器在编译方面非常高效,最终却是一场灾难。得了吧! - Maciej Hehl

0

它允许您拥有这样一种情况,即可以在单独的.h文件中定义迭代器类,其中包括父容器类。由于您已经在迭代器中包含了父标题,因此无法拥有像"getIterator()"这样的方法,因为返回类型必须是迭代器类,因此它需要在父标题中包含迭代器头文件,从而创建循环包含(一个包含另一个,另一个再次包含自己等)。

如果将迭代器类原型放置在父容器内部,则可以拥有这样的方法,而无需包含迭代器头文件。这只起作用,因为您仅仅是说这样的对象存在并将被定义。

当然,有一些解决方法,例如使用预编译头文件,但我认为这样做不太优雅,并带来一堆缺点。当然,这是C++,而不是C。然而,在实践中,您可能会遇到这样一种情况,想以这种方式排列代码,除了类之外。


一个迭代器类,在 C 语言中?问题提到了 C++,但是讨论的是 C 的理论基础,因此我认为需要给出不同的示例 :-) - Steve Jessop

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