C头文件与编译/链接

53
我知道头文件包含各种函数、结构体等的前向声明,这些在调用 #include 的 .c 文件中使用,对吗?就我所理解的,“权力分立”是这样发生的:

头文件:func.h

  • 包含函数的前向声明

int func(int i);

C源代码文件:func.c

  • 包含实际的函数定义

#include "func.h"

int func(int i) {
    return ++i ;
}

C语言源文件source.c(实际程序):

#include <stdio.h>
#include "func.h"

int main(void) {
    int res = func(3);
    printf("%i", res);
}

我的问题是:既然#include只是一个编译器指令,它将.h文件的内容复制到#include所在的文件中,那么.c文件如何知道如何执行函数?它只得到了int func(int i);,那么它怎么实际执行函数呢?它如何获得对func的实际定义的访问权限?头文件是否包含某种“指针”,指出“那就是我的定义,在那里!”?

它是如何工作的?


7
这就是“链接器”的魔力,它可以解析定义并确保在编译期间声明存在的内容实际上存在。 - Uchia Itachi
在处理头文件时,您可能想了解包含保护 - Some programmer dude
我知道关于包含保护(ifndef之类的)但由于简洁起见而省略了它们。 - Aristides
在我看来,你似乎认为源代码被用于执行程序,就像脚本语言(JavaScript等)一样。但事实并非如此。C源代码首先会被编译器和链接器转换成机器码,然后由处理器硬件直接执行。 - zentrunix
2
你说的“源代码被用来执行程序”是什么意思?C语言(或GCC实现)是编译而非解释的。我知道它是预先编译成机器码的。源代码是用来生成机器码的。我不确定你的观点是什么。 - Aristides
5个回答

56

宇智波鼬给出了答案,它是链接器

使用GNU C编译器gcc编译单文件程序的方法如下:

gcc hello.c -o hello # generating the executable hello

但是按照您的示例编译两个(或多个)文件的程序,您需要执行以下操作:

gcc -c func.c # generates the object file func.o
gcc -c main.c # generates the object file main.o
gcc func.o main.o -o main # generates the executable main
每个目标文件都有外部符号(您可以将其视为公共成员)。函数默认情况下是外部的,而(全局)变量默认情况下是内部的。您可以通过定义来更改此行为。
Translated text:

每个目标文件都有外部符号(您可以将其视为公共成员)。函数默认情况下是外部的,而(全局)变量默认情况下是内部的。您可以通过定义来更改此行为。

static int func(int i) { # static linkage
    return ++i ;
}
或者
/* global variable accessible from other modules (object files) */
extern int global_variable = 10; 
当遇到对在主模块中未定义的函数的调用时,链接器会搜索所有提供作为输入的目标文件(和库)以寻找定义了被调用函数的模块。默认情况下,您可能已将某些库链接到程序中,这就是您可以使用“printf”的方式,因为它已经编译为库文件。
如果您真的感兴趣,请尝试一些汇编语言编程。 这些名称相当于汇编代码中的标签。

所以使用GCC的模式是:1. 使用-c标志与每个.c(带有定义)和.h(带有函数原型)一起使用,以创建每个.o 2. 使用-o标志和每个.o文件来创建最终的exe? - Aristides
1
是的,"-c"选项是用于"编译",所以只需将目标代码编译成目标文件。没有-c的gcc会认为输入是目标文件,因此它只是使用链接器将它们链接在一起。最后,-o标志是可选的,它用于指定可执行文件的输出文件名。 - Emil Vatai

19

这是由链接器处理的。编译器只会在目标文件中发出一段特殊序列,表示“我有这个外部符号func,请解决它”给链接器。然后链接器看到这个信息后,就会在所有其他目标文件和库中搜索这个符号。


这是否意味着将搜索项目中的所有“.c”文件? - Lidong Guo
如果您在命令行上编译所有源文件,或者从所有源文件创建目标文件并将它们全部链接起来,那么是的,它们会被搜索。不过这不是自动完成的,您需要告诉链接器要链接哪些目标文件,只有这些文件才会被搜索。@LidongGuo - Some programmer dude
1
@Someprogrammerdude 但是链接器如何从.o文件中知道哪些函数在哪个头文件下被导出,哪些函数在哪个头文件下未解决,符号表中存储了头文件名称或某种哈希值,以确保链接器匹配(即头文件在两个文件中都存在且只有一个目标文件被导出)。 - Lewis Kelsey
头文件在链接中不起作用。目标文件基本上是编译形式的单个翻译单元。链接器知道符号的定义位置,因为目标文件还包含翻译单元中定义的符号。如果您想了解有关链接器如何工作的更多详细信息,建议您在您喜欢的搜索引擎中搜索相关信息,因为这是一个非常广泛的主题,无法在此处回答(特别是在评论中)。 - Some programmer dude

5

在同一编译单元中没有定义符号的声明,告诉编译器使用该符号地址的占位符进行编译生成目标文件。

链接器将检查是否需要该符号的定义,并在库和其他目标文件中查找外部定义。

如果链接器找到了定义,则原始目标文件中的占位符将被替换为最终可执行文件中找到的地址。


2
不仅提供了访问同一程序中其他.c文件的功能,还提供了对以二进制形式分发的库的访问。一个.c文件之间的关系与依赖于另一个库的库完全相同。

由于编程接口需要以文本形式呈现,无论实现格式如何,因此头文件作为关注点的分离是有意义的。

正如其他人所提到的,解决库和源代码(翻译单元)之间的函数调用和访问的程序被称为链接器。

链接器不使用头文件。它只创建一个包含所有翻译单元和库中定义的名称的大表格,然后将这些名称链接到访问它们的代码行上。C语言过时的用法甚至允许在没有任何实现声明的情况下调用函数;只是假定每个未定义的类型都是一个int


2

通常情况下,当您编译这样的文件时:

gcc -o program program.c

你实际上是在调用驱动程序,该程序执行以下操作:
  • 使用cpp进行预处理(如果您要求将其作为单独的步骤)。
  • 使用cc1编译(可能与预处理集成)。
  • 使用as(gas,GNU汇编器)进行汇编。
  • 使用collect2进行链接,它还使用ld(GNU链接器)。
通常,在前3个阶段中,您会创建一个简单的对象文件(.o扩展名),该文件通过编译编译单元(即.c文件,并将#include和其他指令替换为预处理器)创建。
第4个阶段是创建最终可执行文件的阶段。在编译单元编译后,编译器将多个代码片段标记为需要由链接器解析的引用。链接器的工作是在许多编译单元之间搜索并解析对外部编译单元的引用。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接