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

Dan*_*ugg 51 c compilation header-files

我仔细研究了可能的副本,但是没有任何答案在沉没.

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

我试图理解编译器如何理解.c.h文件之间的关系.

鉴于这些文件:

header.h:

int returnSeven(void);
Run Code Online (Sandbox Code Playgroud)

source.c:

int returnSeven(void){
    return 7;
}
Run Code Online (Sandbox Code Playgroud)

main.c:

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

这个混乱会编译吗?我目前正在使用Cygwin的gccNetBeans 7.0中完成我的工作,它可以自动执行大部分构建任务.编译项目时,涉及的项目文件是否根据声明中的隐含包含?source.cheader.h

Jes*_*per 73

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

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

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

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

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

您的函数returnSeven定义在source.c(并且main函数定义在main.c)中.

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

关于联系:

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

  • 谢谢** Jesper **; 您的回答几乎触及了我困惑的所有方面。感谢您的全面回应。 (2认同)
  • 几乎完全无法回答实际问题。它几乎没有提到头文件。这是解释整个imo编译过程的最佳答案之一。 (2认同)

Oli*_*rth 29

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

你的例子应该编译(尽管有愚蠢的语法错误).例如,使用GCC,您可能首先执行以下操作:

gcc -c -o source.o source.c
gcc -c -o main.o main.c
Run Code Online (Sandbox Code Playgroud)

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

然后你做:

gcc -o my_prog source.o main.o
Run Code Online (Sandbox Code Playgroud)

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

  • (注意:这与 main.c 必须能够看到 returnSeven() 的声明这一事实不同:我很迂腐,但这并不完全正确。编译器会愉快地编译(在 C99 中带有警告)这个代码,链接器解析它,通常会产生不好的影响。例如,在文件 ac 中,调用 `x=bob(1,2,3,4)` ,在文件 bc 中,`void bob(char *a){}` 将编译、链接和运行。 (2认同)

pmg*_*pmg 13

编译没有什么神奇之处.也不自动!

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

考虑"hello world"程序(具有更简单的puts功能):

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

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

但是,在这个简单的代码中没有指定函数如何工作.其他人编写了代码puts()并将编译后的代码包含在库中.作为编译过程的一部分,该库中的代码包含在源代码的已编译代码中.

现在考虑你想要自己的版本 puts()

int main(void) {
    myputs("Hello, World!");
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

仅编译此代码会产生错误,因为编译器没有关于该函数的信息.您可以提供该信息

int myputs(const char *line);
int main(void) {
    myputs("Hello, World!");
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

并且代码现在编译---但是没有链接,即不生成可执行文件,因为没有代码myputs().所以你myputs()在一个名为"myputs.c"的文件中编写代码

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

你必须记住编译双方的第一个源文件和"myputs.c"在一起.

过了一会儿,你的"myputs.c"文件已经扩展到一大堆函数,你需要在想要使用它们的源文件中包含有关所有函数(它们的原型)的信息.
将所有原型编写在单个文件和#include该文件中更方便.通过包含,您不会在键入原型时犯错.

您仍然需要编译和链接所有代码文件.


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


Hac*_*Saw 5

头文件用于分隔与源文件中的实现相对应的接口声明。他们以其他方式被滥用,但这是常见的情况。这不是针对编译器,而是针对编写代码的人。

大多数编译器实际上并没有分别查看这两个文件,它们由预处理器组合而成。