简介

本文的组成

本文的内容包含以下几个部分:

  • 一个典型 C/C++ 程序的编译过程
  • 认识静态链接库
  • 认识动态链接库
  • 二者的联系与区别

系列文章

一、编译过程

当我们第一次使用 gcc 这个编译器的时候,一般执行的第一个命令就是:gcc main.c,它将我们的源文件 main.c “编译” 后生成了一个可执行文件:a.out(Windows下为a.exe)。于是,很多人可能就会有一个误解:编译就一个步骤,即编译本身。

但实际上整个编译过程并非只有一个步骤,而是由以下几个步骤组成:

假设我们有一个程序main.c如下所示:

1
2
3
4
5
6
7
#include <stdio.h>
int main(void)
{
printf("Hello World!\n");

return;
}

然后执行以下命令:gcc main.c,然后 gcc 编译器就会执行下面的步骤:

该过程将所有的#define#include等进行替换。

库和头文件的区别

相信很多刚开始学习计算机知识以及刚开始学习 C 语言或 C++ 的小伙伴一定对这两个名词很疑惑,理解起来很混乱。

头文件大家都不陌生,能够通过#include包含的都算是头文件,但是我们在包含<stdio.h>这类标准库的时候,为什么要叫它标准库而不是标准头文件呢。

因为实际上标准库正如其名提供的是一个库文件,这个库文件里也包含着可执行代码,只不过库文件向外的接口往往都是以头文件的形式。其他代码必须要使用这个头文件提供的接口来调用标准库这个库文件中的可执行代码。

将预处理之后的main.c进行编译过程,这个编译过程通常是根据当前的机器将源文件翻译成对应的汇编程序源文件,用后缀.s表示,即main.c被翻译为main.s,这个文件是汇编语言写成的程序。之所以要进行这一步,是因为在不同的机器上,相同指令的机器代码是不同的。我们使用的 C/C++ 语言则是高度抽象的语言,不因机器而异,但是我们最终得到的可执行程序却与机器相关,所以我们首先需要将其转换成与机器相关的文件。汇编语言是机器码的第一层抽象,也与机器相关,所以我们将 C 语言源文件先翻译成汇编语言源文件,然后再生成可执行文件

也就是将上一步我们得到的汇编语言源文件进行汇编得到一个后缀为.o的文件,即main.o,这类文件叫做可重定向目标文件,这个文件里面已经就是我们的机器将要执行的机器码了,是一个二进制文件,但为什么不是a.out或者说是a.exe文件呢?因为整个编译过程还有一个阶段没做

这一步对于 C/C++ 语言的初学者来说好像基本上遇不到,但实际上你编写的几乎每个程序都有这个过程,因为我们免不了使用标准库提供的函数。如果我们使用非标准库也就是第三方库,那么在 gcc 的命令中需要显式地链接这个库文件,这个库文件里就是我们之前包含的一个只有接口没有实现的头文件的实现部分。而只使用标准库的文件为什么在 gcc 命令中不需要显式地链接呢?其原因就是因为几乎每个程序都需要用到标准库提供的函数,而每一次使用 gcc 命令都要显式链接一下标准库实在太麻烦了,所以 gcc 都是隐式地链接了标准库,就不需要我们再动手链接了。在链接好之后我们就可以得到最终的可执行程序了,即a.outa.exe

前面我们说标准库提供的是一个库文件,其中包含着的是可执行代码。实际在链接这一步中,如果你使用了标准库中的函数例如printfscanf等,编译器就会将标准库中的这两个函数对应的可执行代码链接到源文件生成的可重定向目标文件中对应函数调用的位置。

我们可以在终端执行下面的命令来看一下我们最终得到的可执行程序的依赖,也就是它需要链接的库:

1
ldd a.out

可以得到下面的输出:

1
2
3
4
sn@Program:~/桌面/Test$ ldd a.out 
linux-vdso.so.1 (0x00007ffd0315f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fd4e1a41000)
/lib64/ld-linux-x86-64.so.2 (0x00007fd4e1c7d000)

linux-vdso.so.1/lib64/ld-linux-x86-64.so.2在本文中不涉及,中间的/lib/x86-64-linux-gnu/libc.so.6这个文件也就是我们的标准库,里面的内容就是我们使用的标准库函数的实现部分。

二、静态链接库

简介

刚刚我们说链接过程是将库文件中的代码链接到源文件编译出来的可重定向目标文件中使用到该库文件中函数的地方。而这个链接又有两种情况:

  • 其一是将这些库中的可执行代码复制到目标文件中对应位置,即静态链接
  • 第二种是在目标文件对应位置标记上该函数在库文件中的地址,程序运行时会打开库文件查找并运行该地址处的内容,即动态链接

我们可以明显看出,静态链接的优点就是程序运行简单,只需要一个可执行程序。但其缺点也很明显,大量库文件代码复制到可执行程序中,必然导致可执行程序的臃肿,其次是如果某个模块需要进行修改,最后还需要把整个项目编译一遍,极其浪费时间。

生成过程

通常如果要编写一个静态链接库需要按照以下的步骤进行:

  1. 编写接口头文件:我们首先需要思考这个库需要向外提供怎样的接口即功能,然后写成接口头文件
  2. 实现接口:在有了接口头文件之后,我们就需要去实现这些接口,实现的代码被封装,使用者无法看到
  3. 生成库文件:根据接口头文件和接口实现代码,我们就可以生成一个静态链接库文件了
  4. 提供静态链接库:开发者只需向使用者提供接口头文件和生成的静态链接库文件即可,可以看到接口的实现是非必要的,所以才说实现的代码是被封装的

使用过程

如果要使用一个静态链接库通常按照以下的步骤:

  1. 包含接口头文件:在代码中包含接口头文件以使用其提供的接口
  2. 编译时指明静态库的名称和位置以进行链接
  3. 生成可执行程序
  4. 运行可执行程序

三、动态链接库

简介

动态链接库即上述中的另一种情况,在源文件编译出的可重定向目标文件对应位置标记所使用的库函数在库文件中的地址,然后在程序运行时打开库文件查找并运行该地址处的内容。

动态链接库的缺点和静态链接库的优点恰好相反:程序运行时同时需要可执行程序和库文件,可执行程序还必须能够查找到该库文件的位置

而其优点也很明显:最突出的就是其灵活性,如果一个模块出了问题,那么我们只需要重新编译该模块的代码而不需要再次编译整个项目;其次则是可执行程序的体积更小,可执行程序中重复的代码更少。

生成过程

动态链接库的生成过程和静态链接库基本相同,但在生成库文件时稍有差别。

在 Windows 中生成一个动态链接库会 2 个库文件,一个和静态链接库后缀相同的文件,一个动态链接库文件。其中前者虽然和静态链接库后缀相同,但并非静态链接库,而是向目标文件与动态链接库文件联系起来的一个被称作导入库的文件。

在提供动态链接库的时候我们需要同时提供以上两个库文件以及一个接口头文件。

使用过程

动态链接库的使用过程和静态链接库也基本相同,在生成可执行程序时及后续过程有所差别。

同样是在 Windows 系统下,向目标文件提供的需要链接的库文件应该是导入库的名称和地址而非动态链接库的地址(在 Linux 下还是提供动态链接库的地址)。

然后在运行可执行程序时,可执行程序和动态链接库必须同时存在,或者可执行程序能够知道动态链接库的位置(无论是 Windows 还是 Linux)

四、二者的联系与区别

联系

  • 它们都是用来向用户提供一项功能的。通过库文件,我们可以方便地将一些常用的功能封装起来供其他人使用,例如除了标准库之外还有字符编码转换功能、绘图功能等等。用户无需自己实现,只需找到一个优秀的第三方库即可。
  • 都需要提供一个接口头文件供用户导入

区别

  • 它们在链接策略上有所区别
  • 向目标文件导入二者的方式有所区别
  • 向用户提供的文件有所区别
  • 用户使用时的方式有所区别