如果你的计算机用的是 windows 系统,那么库文件将遍布电脑的每一个角落。在 windows 上安装的任何一款软件,其中都一定包含有大量的 .dll 文件(下图为原神目录的部分截图)。

原神目录

.dll 文件称为动态链接库,库包含了写好的,现有的,成熟的,可以复用的代码。作为程序员,需要懂得什么是库,如何生成和使用库,下面让我来一一讲解。

g++编译基础

也许你用过若干种 C++ 编译器,比如 Dev-C++,Visual Studio,这些编译器可以帮我们把写好的代码 .cpp 编译成可执行文件 .exe ,但事实上编译器只是帮我们输入了一句或多句命令,而这些命令就是我们理解文件编译和执行的关键。

我们将一句命令称为命令行,命令行可以在很多地方上执行,比如命令提示符cmd,比如VS-code的终端。如果想要把自己写好的 main.cpp 文件用命令行编译的话,只需要输入下列语句就可以了(记得切换到 main.cpp 的目录):

g++  main.cpp  -o  main

执行上面这条语句之后,就会生成 main.exe 文件,这跟点击 Dev-C++ 的“编译”按钮是一样的。对于这条语句来说,g++ 表示执行 g++.exe 文件,它是用来编译 c++ 文件的;main.cpp 是要被编译的文件;-o 是一个选项,它可以指定生成的文件的名字,后面跟着的 main 就是指定的名字,g++ 会自动补全后面的 .exe,所以最后生成 main.exe,当然写 -o main.exe 也没问题。

想要执行 main.exe ,既可以双击执行,也可以用命令行 ./main 执行。

不过,相信大家多多少少有听过程序编译的一些规范和方法。从代码文件 .cpp 到可执行文件 .exe 其实会经过多个步骤,也会产生多个中间文件。我们重点考虑两个大步骤——编译和链接。

无论是C、C++,首先要把源文件编译成中间代码文件,因为 cpp 文件里面是字符,人能看懂,计算机可看不懂,首先要生成一个对应该 cpp 的能让计算机看得懂的二进制文件,即 Object File ,这个动作叫做编译(compile),然后再把大量的 Object File 合成可执行文件,这个动作叫作链接(link)

于是,上面的g++ main.cpp -o main就可以拆成两句命令:

g++  -c  main.cpp

g++  main.o  -o  main

这两句命令分别对应了编译和链接,前者的-c选项就是让g++生成中间代码文件 main.o,后者则是把多个 .o 文件链接起来,这里只有一个 main.o,下文的介绍将会出现 3 个 .o 文件。

上面这两句命令是本文的核心,全文都会围绕这两句命令进行扩展。

后缀的解释

正常来说,我们将会接触到 6 种文件的后缀:.o.a.so.obj.lib.dll,但其实只有 3 种文件,分别是中间代码文件、静态库文件、动态库文件,只不过这 3 种文件在 windows 系统和 linux 系统上的后缀不一样而已,在 windows 系统上可以兼容所有的 6 种后缀格式,但 linux 系统上只能用 3 种,这 6 种后缀的解释如下:

|windows|linux|文件类型|
|:-:|:-:|:-:|
|.obj|.o|中间代码文件|
|.lib|.a|静态库文件|
|.dll|.so|动态库文件|

简述静态库和动态库的异同

静态库和动态库都是“库”,库指的是多个中间代码文件的集合,也就是说,一个库包含多个 .o 或 .obj 文件,这些 .o 文件里面有可执行的代码。

静态库和动态库的区别在于链接步骤,静态库会在链接时全身心地参与构成可执行文件,链接静态库其实从某种意义上来说也是一种粘贴复制,因为静态库被链接后库就直接嵌入可执行文件中了,整个库的所有代码都会共同构成可执行文件 .exe,假如使用 1 个库生成了 100 个可执行文件,那么就会产生这个库的 100 个副本,这显然非常浪费空间,并且难以修改:一旦发现这个库中的某段代码有 bug,那么那 100 个副本都要修改,即重新生成所有的文件,非常麻烦。

而动态库是在程序运行时被链接的,而只在可执行文件中留下接口的信息(比如函数名),当可执行文件调用接口时,会执行动态库中的代码。这就好像超人给每个人留下了一张名片,上面有电话号码,当有人需要超人的帮助时,就拨打电话呼叫超人,这样既能让每个人都能随时得到超人的帮助,也不需要复制很多个超人,而大家只需要在自己家里放留一张名片,而不是留一个超人,非常的节省空间。

静态库和动态库各有优劣,复制多个副本不见得是坏事,比如你给不懂计算机的同学写一个很简单的程序,那使用静态库的话就可以只发一份可执行文件 .exe 到他电脑上,但如果使用动态库的话还需要把相关的动态库文件发过去,这是其中一方面的考虑。

下面将演示怎么生成和使用两种库,可以模仿着做一做噢!

先不使用库

让我们先创建 5 个用于演示的文件,分别是a.cppa.hb.cppb.hmain.cpp,把他们放在同一个目录中,就像下图一样:

代码目录

然后,再分别往里面填入非常非常非常简单的代码:
a.cpp:

#include <cstdio>

void apple(){
    printf("apple!\n");
}

a.h:

void apple();

b.cpp:

#include <cstdio>

void banana(){
    printf("banana!\n");
}

b.h:

void banana();

main.cpp:

#include "a.h"
#include "b.h"
#include <cstdio>

int main(){
    apple();
    banana();
    int a;
    scanf("%d",&a);
}

填入代码后记得保存,就像下图一样:

代码

main.cpp 中加入scanf是为了在程序中加入阻塞,这样在双击运行 .exe 文件的时候就不会一闪而过。

然后,我们依次在输入以下 4 句命令行并执行,前 3 句是生成 3 个 .cpp 文件对应的 .o 文件,第4句是将 3 个 .o 文件链接成一个可执行文件。

g++  -c  a.cpp
g++  -c  b.cpp
g++  -c  main.cpp
g++  a.o  b.o  main.o  -o  main

每执行上面的一句命令行,都会在当前目录下生成一个新的文件,4句命令生成的文件分别是 a.ob.omain.omain.exe,并且我们可以正常运行main.exe,输出的结果也是正常的,就像下图一样。

linux库(.a和.so)

当然,这一步也能在 windows 上做,是兼容的。

在上一步的代码中可以看出,main.cpp 使用了a.cppb.cpp里的函数,我们把它们分别编译成a.ob.o,然后链接。但如果有 100 个这样的 .o 文件,且它们都起到类似的作用,我们就可以把这些 .o 文件集中在一个静态库 .a 文件中,就像用一个书包把很多本书装起来一样。

让我们先把前面生成的 main.exe删掉,然后执行下面的命令行:

ar  -crv  libfruit.a  a.o  b.o

执行完上面这句,会生成在当前目录中生成 libfruit.a 文件,我们就成功地将a.ob.o放在了一个库中。ar是 linux 中用于生成静态库的工具,请一定要规范静态库的命名,前面以 lib 开头,后面以 .a 结尾,这是必须的。libfruit.a库的真正名字是中间的 fruit.

然后就可以链接这个库来生成可执行文件,让我们执行下面这条命令行:

g++  main.o  libfruit.a  -o  main

这局命令跟之前的g++ a.o b.o main.o -o main长得很像,只不过把a.ob.o替换成了libfruit.a,执行完后一样会生成main.exe,并且也能正常执行,跟之前的链接命令产生了同样的效果!

不过请注意,我们一般不使用g++ main.o libfruit.a -o main这样的用法来链接,这句命令能成功的原因在于库文件就在当前目录下,而我们进行程序设计时,库文件往往放在比较遥远的某处,那再用这种语法来链接的话,每个库文件都要写上绝对路径,非常麻烦。所以我们会用下面的命令行来替代g++ main.o libfruit.a -o main

链接静态库的正宗语法是:

g++  main.o  -o  main  -L.  -lfruit

其中,-L后面跟路径,这样 g++ 就可以到那些目录里面搜索库文件,可以有多个-L,我们这里因为 libfruit.a 就在当前目录下,所以路径用.表示就够了,-l后面跟库名,这是 L 的小写,不是大写的 i,我们前面说过,静态库文件的命名方式是 lib + 库名 + .a,所以libfruit.a的库名是 fruit,g++会自动补齐文件名来寻找库文件,当需要链接多个库时,只需要加多个-l即可。

删掉静态库,不影响可执行文件的运行,这跟动态库有明显的区别。 我们可以试着删除libfruit.a,发现main.exe仍然能正常运行,下面我们来使用动态库。执行下面的命令行:

g++  -shared  -fPIC  -o  libfruit.so  a.o  b.o

动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀lib,但其文件扩展名为 .so。执行上面这句命令行,就会在当前目录中生成动态库文件libfruit.so,然后我们就可以进行链接。有趣的地方是,链接动态库跟链接静态库的方法是一样的,于是,选择执行下面这两句命令行的其中的任意一句,都能生成main.exe

g++  main.o  libfruit.so  -o  main
g++  main.o  -o  main  -L.  -lfruit

让我们直观地感受一下动态库是怎么被使用的,运行 main.exe,记得之前在main.cpp之中有一个scanf函数吗,现在就发挥作用了,它将阻塞程序的运行,阻止程序结束。我们不做输入,让main.exe保持运行状态,然后打开资源监视器,资源监视器可以在任务管理器的“性能”一栏的下方打开,在 win10 中也可以直接在搜索栏中搜索资源监视器打开。

打开资源监视器之后,去到“CPU”一栏,在“进程”中找到“main.exe”,将其勾选(因为名称是按字典序排序的,所以其实很好找),勾选之后,在下方“关联的模块”中,就能看到跟 main.exe 关联的动态库文件,其中就有我们刚刚的libfruit.so,如下图:

除了libfruit.so之外,依赖的其他 .dll 动态库是跟头文件cstdio 有关的,不用太在意,不过这也正说明了,即使是简简单单的程序也不是独立运行的,绝大多数的程序,运行时都会依赖于库。

假如这时我们把 libfruit.so删除,则双击 main.exe 运行时,会报错,提示缺少 libfruit.so无法运行。

还有最后一个疑问,链接静态库和动态库的命令都是g++ main.o -o main -L. -lfruit,那假如同时存在静态库 .a 和动态库 .so 时,会链接哪一个呢?答案是会优先链接动态库,找不到动态库才链接静态库。

windows库(.lib和.dll)

讲完 linux 的静态库和动态库之后,windows 的库只是简单的换个后缀名而已,原理是一模一样的。windows 的静态库的后缀是 .lib,对应前面的 .a;windows 的动态库的后缀是 .dll,对应前面的 .so,下面就只需要单纯地介绍命令行的使用即可。

有一点不同的是,windows 的库不需要添加 lib 前缀,也就是说,库名为 fruit 的静态库文件名就是 fruit.lib,动态库文件名就是 fruit.dll,没有前缀 lib.

使用下面的命令行生成静态库文件:

lib  /OUT:fruit.lib  a.o  b.o

windows 是微软的东西,所以要下载微软的 Visual Studio 才能执行上面的命令行。在上面的语句中,lib指的是运行lib.exe,这是下载 Visual Studio 才有的,/OUT:fruit.lib是指定输出的静态库的文件名,后面跟的是要放进库中的代码文件。

跟 linux 一样,执行下面两句中的任意一句都能链接 fruit.lib 生成 main.exe

g++  main.o  fruit.lib  -o  main
g++  main.o  -o  main  -L.  -lfruit

同样,删掉 fruit.lib 不影响 main.exe 的正常运行。

不过,想要正确生成 dll 动态库的话,我们不仅仅要在命令行中作修改,还要在源文件 .cpp 和 .h 中作修改。请在每个函数前加上 __declspec(dllimport),具体看后面的截图。这是一种函数声明,具体原理比较复杂,先不在这里作解释。

执行下面的命令行可以生成动态库文件 fruit.dll:

g++  -shared  -fPIC  -s  -o  fruit.dll  a.o  b.o

执行下面两句的任意一句可以链接动态库 fruit.dll,生成可执行文件 main.exe

g++  main.o  fruit.dll  -o  main
g++  main.o  -o  main  -L.  -lfruit

跟之前一样,在资源监视器中可以看到main.exe运行时依赖于动态库文件,且删掉fruit.dll之后无法正常运行。

结语

其实还有一个地方没有作对比,就是生成的可执行文件的大小。如果回去观察的话可以发现,使用静态库生成的main.exe会比使用动态库的要大,相差大概 30KB。

至此,静态库和动态库的生成和使用就基本介绍完毕,本文所用到的代码和命令行都是最基础最简单的那一种,不过也足够普通程序员掌握和使用了。代码库是一门很深的学问,本篇 blog 只是简要地介绍,如果对读者能有所帮助,那是我的莫大荣幸,希望你能喜欢。