如果你的计算机用的是 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.cpp
、a.h
、b.cpp
、b.h
、main.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.o
、b.o
、main.o
、main.exe
,并且我们可以正常运行main.exe
,输出的结果也是正常的,就像下图一样。
linux库(.a和.so)
当然,这一步也能在 windows 上做,是兼容的。
在上一步的代码中可以看出,main.cpp
使用了a.cpp
和b.cpp
里的函数,我们把它们分别编译成a.o
和b.o
,然后链接。但如果有 100 个这样的 .o 文件,且它们都起到类似的作用,我们就可以把这些 .o 文件集中在一个静态库 .a 文件中,就像用一个书包把很多本书装起来一样。
让我们先把前面生成的 main.exe
删掉,然后执行下面的命令行:
ar -crv libfruit.a a.o b.o
执行完上面这句,会生成在当前目录中生成 libfruit.a
文件,我们就成功地将a.o
和b.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.o
和b.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 只是简要地介绍,如果对读者能有所帮助,那是我的莫大荣幸,希望你能喜欢。