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

时间:2011-05-05 21:52:48

标签: c compilation header-files

我仔细阅读了可能的副本,但是没有任何答案在沉没。

tl; dr:C中的源文件和头文件有何关联?项目是否在构建时隐式地整理声明/定义依赖性?

我正在尝试了解编译器如何理解 .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 中完成我的工作,这可以自动执行大部分构建任务。编译项目时,涉及的项目文件是否会根据source.c中的声明对header.h的隐式包含进行排序?

5 个答案:

答案 0 :(得分:67)

将C源代码文件转换为可执行程序通常分两步完成:编译链接

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

在第一步中,编译器采用编译单元,它通常是一个预处理的源文件(因此,一个源文件包含所有标题的内容#include s并将其转换为目标文件。

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

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

您的功能returnSevensource.c中定义(main功能在main.c中定义)。

因此,总而言之,您有两个编译单元:source.cmain.c(包含它的头文件)。您可以将它们编译为两个目标文件:source.omain.o。第一个将包含returnSeven的定义,第二个定义为main的定义。然后,链接器将在可执行程序中将这两者粘合在一起。

关于联系:

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

答案 1 :(得分:26)

C语言没有源文件和头文件的概念(编译器也没有)。这只是一个惯例;记住头文件始终是#include d到源文件中;在适当的编译开始之前,预处理器只是复制粘贴内容。

您的示例应该编译(尽管有愚蠢的语法错误)。例如,您可以使用GCC:

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

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

然后你做:

gcc -o my_prog source.o main.o

将两个目标文件链接在一起成为可执行二进制文件,并执行符号解析。在我们的示例中,这是可能的,因为main.o需要returnSeven(),这由source.o公开。如果一切都不匹配,将导致链接器错误。

答案 2 :(得分:13)

编译没有什么神奇之处。也不是自动的!

头文件基本上为编译器提供信息,几乎从不编码 仅此信息通常不足以创建完整的程序。

考虑“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该文件中更方便。通过包含,您不会在键入原型时犯错。

你仍然需要编译和链接所有代码文件。


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

答案 3 :(得分:4)

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

大多数编译器实际上并不单独看到这两个文件,它们由预处理器组合在一起。

答案 4 :(得分:2)

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

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