可能是一个愚蠢的(而且非常简单)问题,但我想尝试一下,因为我不知道在哪里可以找到答案。我意识到一些书,我开始谷歌一些东西-我实际上有点好奇为什么,如果我们有这样的文件:
file1.c
#include <stdio.h>
#include "file2.h"
int main(void){
printf("%s:%s:%d n", __FILE__, __FUNCTION__, __LINE__);
foo();
return 0;
}
file2.h
void foo(void);
和
file2.c
#include <stdio.h>
#include "file2.h"
void foo(void) {
printf("%s:%s:%d n", __FILE__, __func__, __LINE__);
return;
}
用:
编译gcc file1.c file2.c -o file -Wall
为什么包括file2.h
的头文件是一个很好的做法,其中包含foo
函数的原型在同一文件中,foo
被声明?我完全理解将其附加到file1.c
,而我们应该使用头文件来定义每个模块的接口,而不是将其写入"raw",但是为什么将带有原型的头文件附加到声明它的文件(file2.c
)?-墙选项标志也没有说什么,如果我不包括它,那么为什么人们说它是"正确的方式"?它是否有助于避免错误,或者只是为了更清晰的代码?
这些代码示例取自下面的讨论:在一个程序中编译多个C文件
一些用户说这是"正确的方式"。
要回答这个问题,您应该对编译器和链接器之间的区别有一个基本的了解。简而言之,编译器单独编译每个翻译单元(C文件),然后链接器的工作是将所有编译后的文件链接在一起。
例如,在上面的代码中,链接器是查找从main()
调用的函数foo()
存在的位置并链接到它的链接器。
首先是编译器步骤,然后是链接器步骤。
让我们来演示一个例子,在file2.c中包含file2.h是很方便的:
file2.h
void foo(void);
file2.c
#include <stdio.h>
#include "file2.h"
void foo(int i) {
printf("%s:%s:%d n", __FILE__, __func__, __LINE__);
return;
}
这里foo()
的原型与其定义不同。
通过在file2.c中包含file2.h,这样编译器就可以检查函数的原型是否等同于它的定义,如果不是,那么你将得到一个编译器错误。
如果file2.h
不包含在file2.c
中会发生什么
那么编译器不会发现任何问题,我们必须等到链接步骤,当链接器发现从main()
调用的foo()
函数没有匹配时,它会通过一个错误。
如果链接器稍后会发现错误,那么为什么要麻烦呢?
因为在大型解决方案中,可能有数百个源代码需要花费大量时间来编译,因此等待链接器在最后引发错误将浪费大量时间。
这是唯一真正的原因TM:
如果编译器遇到没有原型的函数调用,则从该调用派生一个原型,参见标准章6.5.2.2第6段。如果这与实际函数的接口不匹配,则是未定义行为在大多数情况下。顶多没有害处,但什么事都有可能发生。
只有足够高的警告级别,编译器才会发出警告或错误之类的诊断。这就是为什么您应该始终使用尽可能高的警告级别,并且在实现文件中包含头文件的原因。. 您一定不想错过这个让您的代码被自动检查的机会。
C通常不会混淆符号(有一些例外,例如:在Windows上)。扭曲的符号将携带类型信息。没有它,链接器相信你没有犯错误。
如果不包含头文件,则可以声明该符号为一种形式,然后将其定义为其他形式。如。在头文件中,您可以将foo
声明为一个函数,然后在源文件中,您可以将其定义为一个完全不兼容的函数(不同的调用约定和签名),甚至根本不是函数-例如全局变量。这样的项目可能会链接,但不会是功能性的。错误实际上可能是隐藏的,因此,如果您没有适当的可靠测试,那么在客户通知您之前,您将无法捕获它。或者更糟的是,有一篇关于它的新闻文章。
在c++中,符号携带有关其类型的信息,所以如果你声明了一个东西,然后定义了具有相同基名但类型不兼容的东西,链接器将拒绝链接项目,因为引用了特定的符号但从未定义。
所以,在C中,你包含头文件来防止工具无法捕获的错误,这将导致二进制文件被破坏。在c++中,您这样做是为了在编译期间而不是稍后的链接阶段得到一个错误。