C中的头文件和源文件如何工作?
我已经仔细阅读了可能的重复内容,但是没有一个答案是没有的。
tl; dr: C
中的源文件和头文件是如何相关的? 项目在构build时隐式地清理声明/定义依赖项吗?
我想了解编译器如何理解 .c
和.h
文件之间的关系。
鉴于这些文件:
header.h :
int returnSeven(void);
source.c :
int returnSeven(void){ return 7; }
main.c :
#include <stdio.h> #include <stdlib.h> #include "header.h" int main(void){ printf("%d", returnSeven()); return 0; }
这个混乱会编译? 我目前正在使用Cygwin中的gcc来完成我在NetBeans 7.0中的工作,它可以自动执行许多构build任务。 当一个项目被编译时,涉及到的项目文件会根据header.h
的声明,将这个隐含的source.c
整理出来。
将C源代码文件转换为可执行程序通常分两步完成: 编译和链接 。
首先,编译器将源代码转换为目标文件( *.o
)。 然后,链接程序将这些目标文件与静态链接的库一起,并创build一个可执行程序。
在第一步中,编译器需要一个编译单元 ,它通常是一个预处理的源文件(所以,源文件包含所有包含它的头文件的内容)并将其转换为目标文件。
在每个编译单元中,必须声明所有使用的函数,以使编译器知道该函数是否存在以及它的参数是什么。 在你的例子中,函数returnSeven
的声明在头文件header.h
。 当你编译main.c
,你需要在头文件中包含声明,以便编译器在编译main.c
时知道returnSeven
存在。
当链接器完成其工作时,需要find每个函数的定义 。 每个函数都必须在一个目标文件中定义一次 – 如果有多个包含相同函数定义的目标文件,链接器将停止并返回错误。
您的函数returnSeven
在source.c
定义( main
函数在main.c
定义)。
所以总结一下,你有两个编译单元: source.c
和main.c
(包含它所包含的头文件)。 你将这些编译成两个目标文件: source.o
和main.o
第一个包含returnSeven
的定义,第二个包含main
的定义。 然后链接器将这两个在可执行程序中粘在一起。
关于链接:
有外部联系和内部联系 。 默认情况下,函数具有外部链接,这意味着编译器使这些function对链接器可见。 如果你使一个函数成为static
,那么它就有内部链接 – 只有在它被定义的编译单元中才能看到(链接器不知道它存在)。 这对于在源文件中内部执行某些操作的函数是有用的,并且您希望隐藏程序的其余部分。
C语言没有源文件和头文件的概念(编译器也没有)。 这只是一个惯例, 记住一个头文件总是#include
d到一个源文件中; 在正确编译开始之前,预处理器实际上只是复制粘贴内容。
你的例子应该编译(尽pipe有愚蠢的语法错误)。 例如,使用GCC,您可能会先执行以下操作:
gcc -c -o source.o source.c gcc -c -o main.o main.c
这将分别编译每个源文件,创build独立的目标文件。 在这个阶段, returnSeven()
在main.c
没有被parsing。 编译器只是以一种说明必须在将来解决的方式标记目标文件。 所以在这个阶段, main.c
不能看到returnSeven()
的定义 。 (注意:这与main.c
必须能够看到returnSeven()
的声明才能编译的事实不同,它必须知道它确实是一个函数,它的原型是什么,这就是为什么你必须在main.c
#include "source.h"
)
你然后做:
gcc -o my_prog source.o main.o
这将两个目标文件链接到一个可执行的二进制文件中,并执行符号的parsing。 在我们的例子中,这是可能的,因为main.o
需要main.o
returnSeven()
,这是由source.o
公开的。 在一切不匹配的情况下,会导致链接器错误。
编译没有什么魔力。 也不自动!
头文件基本上向编译器提供信息,几乎从不代码。
仅仅这些信息通常不足以创build一个完整的程序。
考虑“hello world”程序(使用简单的puts
函数):
#include <stdio.h> int main(void) { puts("Hello, World!"); return 0; }
没有头文件,编译器不知道如何处理puts()
(它不是C关键字)。 头让编译器知道如何pipe理参数和返回值。
然而,这个简单的代码中没有指定函数的工作方式。 其他人已经编写了puts()
的代码, puts()
编译的代码包含在一个库中。 作为编译过程的一部分,该库中的代码包含在编译后的源代码中。
现在考虑你想要你自己的puts()
int main(void) { myputs("Hello, World!"); return 0; }
编译这个代码给出了一个错误,因为编译器没有关于该函数的信息。 你可以提供这些信息
int myputs(const char *line); int main(void) { myputs("Hello, World!"); return 0; }
并且代码现在编译—但是不链接,即不生成可执行文件,因为没有myputs()
代码。 所以你把myputs()
的代码写在一个名为“myputs.c”的文件中
#include <stdio.h> int myputs(const char *line) { while (*line) putchar(*line++); return 0; }
你必须记得把你的第一个源文件和“myputs.c”一起编译。
过了一段时间,你的“myputs.c”文件已经扩展为一个完整的函数,你需要在源文件中包含所有函数(它们的原型)的信息,以便使用它们。
将所有原型写入单个文件和#include
该文件更为方便。 包含你在运行原型时不会冒犯错的风险。
你仍然需要编译和链接所有的代码文件。
当他们成长得更多的时候,你把所有已经编译好的代码放在一个库里…这就是另一回事:)
头文件用于分隔与源文件中的实现对应的接口声明。 他们以其他方式受到虐待,但这是常见的情况。 这不是编译器,而是编写代码的人。
大多数编译器实际上不会单独看到这两个文件,它们是由预处理器组合的。
编译器本身对源文件和头文件之间的关系没有特定的“知识”。 这些types的关系通常由项目文件(例如,makefile,解决scheme等)定义。
给出的例子看起来好像它会正确编译。 您需要编译这两个源文件,然后链接器需要两个目标文件来生成可执行文件。