C 语言中的头文件和源文件是如何工作的?

我仔细研究了可能存在的副本,但是没有一个答案是可以理解的。

Dr: 在 C中源文件和头文件是如何相关的? 项目是否在构建时隐式地排序声明/定义依赖关系?

我想了解一下编译器 明白如何处理 .c.h文件之间的关系。

根据这些文件:

标题 h :

int returnSeven(void);

资料来源:

int returnSeven(void){
return 7;
}

C :

#include <stdio.h>
#include <stdlib.h>
#include "header.h"
int main(void){
printf("%d", returnSeven());
return 0;
}

这个烂摊子能收拾好吗?我目前正在 Cygwin 的 GCC中使用 NetBeans 7.0完成工作,它可以自动完成大部分的构建任务。当一个项目被编译时,所涉及的项目文件是否会根据 header.h中的声明来解决这种隐含的包含 source.c的问题?

76849 次浏览

C 语言没有源文件和头文件的概念(编译器也没有)。这仅仅是一个约定; 请记住,头文件总是 #included 到源文件中; 预处理程序实际上只是在正确的编译开始之前复制粘贴内容。

例如 应该编译(尽管存在愚蠢的语法错误):

gcc -c -o source.o source.c
gcc -c -o main.o main.c

这将分别编译每个源文件,创建独立的对象文件。在这个阶段,returnSeven()还没有在 main.c内部解析; 编译器只是标记了目标文件,声明将来必须解析它。所以在这个阶段,main.c看不到 returnSeven()定义不是问题。(注意: 这与 main.c必须能够看到 returnSeven()声明才能编译的事实不同; 它必须知道它确实是一个函数,以及它的原型是什么。这就是为什么你必须在 main.c#include "source.h"。)

然后你就可以:

gcc -o my_prog source.o main.o

这个 链接把两个对象文件合并成一个可执行的二进制文件,并执行符号解析。在我们的示例中,这是可能的,因为 main.o需要 returnSeven(),而这是由 source.o公开的。在所有内容都不匹配的情况下,将导致链接器错误。

编译器本身对源文件和头文件之间的关系没有特定的“知识”。这些类型的关系通常由项目文件(例如,makefile、 Solution 等)定义。

给定的示例看起来似乎可以正确编译。您需要编译这两个源文件,然后链接器需要这两个目标文件来生成可执行文件。

头文件用于分隔与源文件中的实现对应的接口声明。他们在其他方面受到虐待,但这是常见的情况。这不是为了编译器,而是为了编写代码的人。

大多数编译器实际上不会分别看到这两个文件,它们是由预处理器组合在一起的。

将 C 源代码文件转换为可执行程序通常分为两个步骤: 编译中连接

首先,编译器将源代码转换为目标文件(*.o)。然后,链接器将这些目标文件连同静态链接的库一起创建一个可执行程序。

在第一步中,编译器获取一个 编译单元,它通常是一个预处理的源文件(即包含所有 #include头文件内容的源文件) ,并将其转换为一个目标文件。

在每个编译单元中,所有使用的函数都必须是 声明,以便让编译器知道函数的存在以及它的参数是什么。在您的示例中,函数 returnSeven的声明位于头文件 header.h中。编译 main.c时,在声明中包含标头,以便编译器在编译 main.c时知道 returnSeven的存在。

当链接器完成它的工作时,它需要找到每个函数的 定义。每个函数必须在一个目标文件中精确定义一次-如果有多个目标文件包含同一个函数的定义,链接器将停止一个错误。

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

因此,总结一下,您有两个编译单元: source.cmain.c(包含头文件)。将它们编译成两个对象文件: source.omain.o。第一个包含 returnSeven的定义,第二个包含 main的定义。然后链接器将这两者粘合到一个可执行程序中。

关于联网:

外部连接装置内部连接装置内部连接装置。默认情况下,函数具有外部链接,这意味着编译器使这些函数对链接器可见。如果你创建一个函数 static,它有内部链接——它只在定义它的编译单元中可见(链接器不会知道它的存在)。这对于在源文件内部执行某些操作并且希望对程序的其余部分进行隐藏的函数非常有用。

编译没有任何魔力,也不是自动的!

头文件基本上向编译器提供信息,几乎从不编写代码。
仅仅这些信息,通常不足以创建一个完整的程序。

考虑一下“ hello world”程序(使用更简单的 puts函数) :

#include <stdio.h>
int main(void) {
puts("Hello, World!");
return 0;
}

没有头,编译器不知道如何处理 puts()(它不是 C 关键字)。报头让编译器知道如何管理参数和返回值。

但是,在这段简单的代码中并没有指定函数的工作方式。其他人编写了 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.c”的文件中编写 myputs()的代码

#include <stdio.h>
int myputs(const char *line) {
while (*line) putchar(*line++);
return 0;
}

你必须记住编译 都有你的第一个源文件和“ myputs.c”在一起。

过了一会儿,“ myputs.c”文件已经扩展到了一手函数,您需要在源文件中包含所有希望使用它们的函数(它们的原型)的信息。
更方便的方法是将所有原型写入一个文件中,并将该文件写入 #include。使用这个包含,您在输入原型时不会有出错的风险。

但是,您仍然需要编译并链接所有代码文件。


当它们增长得更多时,您将所有已编译的代码放入一个库中... ... 这是另一个故事:)