- 1) 预处理器、链接器的区别是什么? - 2) 头文件、库文件的区别是什么? 我的理解正确吗?

21

好的,直到今天早上我一直对这些术语感到非常困惑。我想我已经明白了它们之间的区别,希望如此。

首先,令我感到困惑的是,预处理器已经将包含函数的头文件包括在代码中,那么链接器会将哪些库函数链接到汇编器/编译器产生的目标文件呢?这部分困惑主要是由于我对头文件和库之间的区别不了解。

通过谷歌和stackoverflow(这是一个术语吗? :p),我收集到的信息是,头文件主要包含函数声明,而实际实现在另一个二进制文件中,称为库(我仍然不确定这一点)。

所以,假设在以下程序中:

#include<stdio.h>
int main()
{
      printf("whatever");
      return 0;
}

预处理器在代码中包含了头文件的内容。编译器/汇编器执行它们的工作,最后链接器将这个目标文件与另一个实际存储printf()工作方式的目标文件组合起来。

我的理解是否正确?我可能完全错了...所以你能帮助我吗?

编辑:我一直对C++ STL感到困惑。它是否真的是所有那些头文件的集合呢?现在在阅读回答后,我可以说STL是一个类似于目标文件的东西吗?

我也想知道像pow()sqrt()等函数的函数定义在哪里可以读取。我会打开头文件但找不到任何信息。那么,函数定义是以二进制不可读的形式保存在库中吗?


您在代码下面提到的理解是正确的。 - Alok Save
最后生成一个可执行文件或另一个库.... ;) - perilbrain
一个关于C语言的解释(但C++基本相同)可以在我的C教程中找到,在头文件章节中:http://masters-of-the-void.com/book10.htm - uliwitness
3个回答

26
一个C源文件经历两个主要阶段:(1) 预处理阶段,在此阶段中,C源代码通过预处理器工具进行处理,该工具查找预处理指令并执行这些操作;(2) 编译阶段,在此阶段中,处理后的C源代码实际上被编译以生成目标代码文件。
预处理器是一个执行文本操作的实用程序。它将包含可能包含预处理指令的文本(通常是C源代码)的文件作为输入,并输出一个修改后的版本,通过应用任何找到的指令来生成一个文本输出。
文件不必是C源代码,因为预处理器执行文本操作。我曾看到C预处理器被用于扩展make实用程序,通过允许在make文件中包括预处理器指令。带有C预处理器指令的make文件通过C预处理器实用程序运行,生成的输出然后被馈入make以执行目标构建。
库和链接
库是包含各种函数的目标代码文件。这是一种将多个源文件的输出编译成单个文件的方式。通常,伴随着库文件的提供一起提供了一个头文件(包含文件),通常具有.h文件扩展名。头文件包含函数声明、全局变量声明以及库所需的预处理器指令。因此,要使用库,您需要使用#include指令包含提供的头文件,并与库文件链接。
库文件的一个好处是,您提供了源代码的已编译版本,而不是源代码本身。另一方面,由于库文件包含已编译的源代码,用于生成库文件的编译器必须与用于编译自己的源代码文件的编译器兼容。
通常使用两种类型的库。第一种和较旧的类型是静态库。第二种和较新的类型是动态库(在Windows中为Dynamic Link Library或DLL,在Linux中为Shared Library或SO)。两种类型之间的区别在于将库中的函数绑定到使用库文件的可执行文件时的时间点。
链接器是一个实用程序,它将各种目标文件和库文件组合起来创建可执行文件。当在C源文件中使用外部或全局函数或变量时,使用一种标记告诉链接器需要在那个点插入函数或变量的地址。
C编译器只知道它编译的源文件中的内容,而不知道其他文件(例如目标文件或库)中的内容。因此,链接器的工作是获取各种对象文件和库并通过替换标记与实际连接线之间建立最终连接。因此,链接器是一个“链接”各种组件的实用程序,它用实际为该全局函数或变量生成的对象代码的链接替换对象文件和库中全局函数或变量的标记。在链接器阶段,静态库和动态或共享库之间的区别变得明显。当使用静态库时,库的实际目标代码包含在应用程序可执行文件中。当使用动态或共享库时,应用程序可执行文件中包含的对象代码是用于查找共享库并在运行应用程序时连接它的代码。 在某些情况下,相同的全局函数名称可能在几个不同的目标文件或库中使用,因此链接器通常只使用它遇到的第一个,并发出有关其他找到的警告。
编译和链接C程序的基本过程如下: 1. 预处理器生成要编译的C源代码 2. 编译器将C源代码编译成目标代码生成一组目标文件。 3. 链接器将各种目标文件以及任何库链接到可执行文件中。
上述是基本过程,但是当使用动态库时,它可能会变得更加复杂,尤其是如果正在生成的应用程序的一部分具有它正在生成的动态库。
还有一个阶段是当应用程序实际加载到内存并开始执行。操作系统提供了一个实用程序——加载器,它读取应用程序可执行文件并将其加载到内存中,然后启动应用程序运行。可执行文件的起始点或入口点在可执行文件中指定,因此在加载器将可执行文件读入内存后,它将通过跳转到入口点内存地址开始运行可执行文件。
链接器可能会遇到的一个问题是,有时它在处理目标代码文件时可能会遇到需要实际内存地址的标记。然而,链接器不知道实际的内存地址,因为该地址将根据应用程序加载到内存的位置而变化。因此,当加载器将可执行文件加载到内存并准备好启动它运行时,链接器将该标记标记为需要修复的内容。
最后一个主题是C Runtime和main()以及可执行文件的入口点。 C Runtime是编译器制造商提供的包含用C编写的应用程序的入口点的目标代码。main()函数是由编写应用程序的程序员提供的入口点,但这不是加载器看到的入口点。在应用程序启动之后,C Runtime代码调用main()函数,并设置应用程序的环境。

C Runtime并不是标准的C库。C运行时库的目的是管理应用程序的运行环境。标准C库的目的是提供一组有用的工具函数,使程序员不必自己创建。

当加载器加载应用程序并跳转到C Runtime提供的入口点时,C Runtime执行所需的各种初始化操作,以为应用程序提供适当的运行时环境。完成此操作后,C Runtime调用main()函数,使应用程序开发人员或程序员创建的代码开始运行。当main()返回或调用exit()函数时,C Runtime执行任何需要清理和关闭应用程序的操作。


感谢您提供如此详细的解释。正如所提到的 - “链接器可能遇到的一个问题是,在处理目标代码文件时,有时可能会遇到需要实际内存地址的标记。”您能否举个例子,说明需要实际内存地址的情况? - Yug Singh
@YugSingh 我想到的一个问题是当一个 DLL 被加载时。我在思考你的问题,想知道这个答案的一部分是否需要更新,因为虚拟内存和 MMU 技术现在甚至在嵌入式处理器中也很常见。 - Richard Chambers

3

这是一个非常常见的困惑源。我认为理解正在发生的事情最简单的方法是举一个简单的例子。暂时忘记库,考虑以下内容:

$ cat main.c
extern int foo( void );
int main( void ) { return foo(); }
$ cat foo.c
int foo( void ) { return 0; }
$ cc -c main.c
$ cc -c foo.c
$ cc main.o foo.o

声明extern int foo( void )的作用和库的头文件完全相同。foo.o执行库的功能。如果你理解了这个例子以及为什么cc main.ccc main.o都不起作用,那么你就理解了头文件和库之间的区别。


谢谢。我不确定我完全理解这个例子...但这是因为我不懂“extren”这个词。稍后我会对它进行研究,如有疑虑再来打扰你。 :) - Paagalpan
似乎正在使用cat Linux命令创建两个小型、简单的C源代码文件main.c和foo.c,每个文件都首先编译然后链接。cc命令具有足够的智能性,如果您指定了目标文件main.o和foo.o文件,它将仅使用这些文件执行链接。 - Richard Chambers
“cat” 不会创建文件,只是显示它们。 - William Pursell

2

是的,几乎正确。除了链接器不仅连接目标文件,还连接库——在这种情况下,C标准库(libc)是连接到您的目标文件中的内容。您对编译阶段和头文件与库之间的区别的其余假设似乎是正确的。


@DevSolar 除了它并不是。它是由目标文件创建的,但它本身绝对不是一个目标文件。 - user529758
@H2CO3:共享库通常被视为自己的目标文件。 - Fred Foo
@H2CO3:不是静态库,而是共享库。静态库是普通对象文件的“ar”归档文件。共享库是“共享对象”文件(在Linux和一些其他系统上使用“.so”扩展名)。 - Fred Foo
@larsmans 我知道这一点。但从技术上讲,尽管它们有“共享对象”这个名称,它们实际上不是对象文件。它们是使用链接器正确地链接的,就像可执行文件一样,并且可以打开以直接执行其内容(例如,dlopen() API),因为它们包含对象文件没有的位置/地址信息。 - user529758
@H2CO3: dlopen() 以 "dl" 开头,因为它是对 动态链接装载器 的调用。参考 Windows 上的 ".dll"(动态链接库)... - DevSolar
显示剩余3条评论

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