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每个函数的定义 。 每个函数都必须在一个目标文件中定义一次 – 如果有多个包含相同函数定义的目标文件,链接器将停止并返回错误。

您的函数returnSevensource.c定义( main函数在main.c定义)。

所以总结一下,你有两个编译单元: source.cmain.c (包含它所包含的头文件)。 你将这些编译成两个目标文件: source.omain.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等)定义。

给出的例子看起来好像它会正确编译。 您需要编译这两个源文件,然后链接器需要两个目标文件来生成可执行文件。