动静态库
库的介绍
在C语言当中,是库的主要存在形式之一,其中,表示标准输入/输出库,它是一种函数库,包含但不仅限于以下内容:
- 预定义的函数操作:"printf" 格式化输出,"scanf" 格式化输入等。
- 预定义流:"stdin"标准输入流,通常关联到键盘,"stdout"标准输出流,通常关联到终端或命令行窗口(控制台)等。
什么是库
库是已经写好的、成熟的、可复用的代码。每个程序都需要依赖很多底层库,不可能每个人的代码从零开始编写代码,因此库的存在具有非常重要的意义。
在我们的开发的应用中经常有一些公共代码是需要反复使用的,就把这些代码编译为库文件。
库可以简单看成一组目标文件的集合,将这些目标文件经过压缩打包之后形成的一个文件。像在Windows这样的平台上,最常用的C语⾔库是集成开发环境所附带的运行库,这些库一般由编译⼚商提供。
库(Library,一般简称lib)在任何编程语言当中都是很重要的概念。它通常就是由预先定义的数据、数据结构以及操作等组成的集合,使用者可以在不关心"库"内部设计细节的前提下,直接使用库中的资源。库的存在加速了开发过程,提高代码的可重用性和维护性。
库是二进制文件,是源代码文件的另一种表现形式,是加了密的源代码;是一些功能相近或者是相似的函数的集合体。
使用库有什么好处
提高代码的可重用性,而且还可以提高程序的健壮性;
可以减少开发者的代码开发量,缩短开发周期。
如何使用库
不同编程语言的库肯定是不同的,使用方式也大相径庭。那么在C语言当中,如何使用库呢?
使用预处理指令:#include <xxx.h>
头文件——包含了库函数的声明
库文件——包含了库函数的代码实现
注意
库不能单独使用,只能作为其他执行程序的一部分完成某些功能,也就是说只能被其他程序调用才能使用。
库的工作原理
静态库如何被加载
在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。
动态库如何被加载
在程序编译的最后一个阶段也就是链接阶段:
- 在gcc命令中虽然指定了库路径(使用参数
-L
), 但是这个路径并没有记录到可执行程序中,只是检查了这个路径下的库文件是否存在。 - 同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字。
可执行程序被执行起来之后:
- 程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息
- 当动态库中的函数在程序中被调用了, 这个时候动态库才加载到内存,如果不被调用就不加载
- 动态库的检测和内存加载操作都是由动态连接器来完成的
动态链接器
动态链接器是一个独立于应用程序的进程, 属于操作系统, 当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L
指定的路径。
那么动态链接器是如何搜索某一个动态库的呢,在它内部有一个默认的搜索顺序,按照优先级从高到低的顺序分别是:
- 可执行文件内部的
DT_RPATH
段 - 系统的环境变量
LD_LIBRARY_PATH
- 系统动态库的缓存文件
/etc/ld.so.cache
- 存储动态库/静态库的系统目录
/lib/
,/usr/lib
等
按照以上四个顺序, 依次搜索, 找到之后结束遍历, 最终还是没找到, 动态连接器就会提示动态库找不到的错误信息。
静态库
静态库——(static library)
静态库可以认为是一些目标代码的集合,是在可执行程序运行前就已经加入到执行码中,成为执行程序的一部分。按照习惯,一般以.a
做为文件后缀。
静态库的命名一般分为三个部分:
前缀:
lib
库名称:自定义即可,如
test
后缀:
.a
所以最终的静态库的名字应该为:libtest.a
在Linux环境中
在Linux中静态库由程序 ar 生成,现在静态库已经不像之前那么普遍了,这主要是由于程序都在使用动态库。关于静态库的命名规则如下:
- 在Linux中静态库以lib作为前缀,以
.a
作为后缀, 中间是库的名字自己指定即可,即:libxxx.a
- 在Windows中静态库一般以lib作为前缀,以
.lib
作为后缀, 中间是库的名字需要自己指定,即:libxxx.lib
创建静态库
生成静态库,需要先对源文件进行汇编操作 (使用参数 -c
) 得到二进制格式的目标文件 (.o
格式), 然后在通过 ar工具将目标文件打包就可以得到静态库文件了 (libxxx.a
)。
使用ar工具创建静态库的时候需要三个参数:
- 参数
c
:创建一个库,不管库是否存在,都将创建。 - 参数
s
:创建目标文件索引,这在创建较大的库时能加快时间。 - 参数
r
:在库中插入模块(替换)。默认新的成员添加在库的结尾处,如果模块名已经在库中存在,则替换同名的模块。
下面以fun1.c
,fun2.c
和head.h
三个文件为例讲述静态库的制作和使用,其中head.h
文件中有函数的声明,fun1.c
和fun2.c
中有函数的实现。
步骤1:将c源文件生成对应的.o文件
gcc -c fun1.c fun2.c
- 或者分别生成.o文件:
gcc -c fun1.c -o fun1.o
gcc -c fun2.c -o fun2.o
步骤2:使用打包工具ar将准备好的.o文件打包为.a文件。在使用ar工具是时候需要添加参数rcs
r
更新、c
创建、s
建立索引- 命令:
ar rcs 静态库名 .o文件
ar rcs libtest1.a fun1.o fun2.o
- 命令:
步骤3:发布静态库
- 提供头文件
**.h
- 提供制作出来的静态库
libxxx.a
- 提供头文件
使用静态库
静态库制作完成之后,
假设测试文件为main.c
,静态库文件为libtest1.a
,头文件为head.h
用到的参数:
-L
:指定要连接的库的所在目录-l
:指定链接时需要的静态库,去掉前缀和后缀-I
: 指定main.c
文件用到的头文件head.h
所在的路径
gcc -o main1 main.c -L./ -ltest1 -I./
在Windows环境中
参考链接 Microsoft演练:创建并使用静态库
创建静态库
创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和解决方案名称中输入staticlib。点击确定。
在解决方案资源管理器的头文件中添加
mylib.h
文件,在源文件添加mylib.c
文件(即实现文 件)。在
mylib.h
文件中添加如下代码:
#ifndef TEST_H
#define TEST_H
int myadd(int a,int b);
#endif
- 在
mylib.c
文件中添加如下代码:
#include"test.h"
int myadd(int a, int b){ return a + b; }
配置项目属性。因为这是一个静态链接库,所以应在项目属性的“配置属性”下选择“常规”, 在其下的配置类型中选择“静态库(.lib)。
编译生成新的解决方案,在Debug文件夹下会得到 mylib.lib (对象文件库),将该.lib文件和 相应头文件给用户,用户就可以使用该库里的函数了。
VS2022创建静态库
使用静态库
方法一:配置项目属性
添加工程的头文件目录:工程 ➡️ 属性 ➡️ 配置属性 ➡️ c/c++ ➡️ 常规 ➡️ 附加包含目录:加上头文件存放目录。
添加文件引用的lib静态库路径:工程 ➡️ 属性 ➡️ 配置属性 ➡️ 链接器 ➡️ 常规 ➡️ 附加库目录:加上lib文件存放目录。
然后添加工程引用的lib文件名:工程 ➡️ 属性 ➡️ 配置属性 ➡️ 链接器 ➡️ 输入 ➡️ 附加依赖项: 加上lib文件名。
方法二:使用编译语句
#pragma comment(lib,"./mylib.lib")
方法三:添加工程中
就像你添加.h
和.c
文件一样,把lib文件添加到工程文件列表中去
切换到"解决方案视图" ➡️ 选中要添加lib的工程 ➡️ 点击右键 ➡️ "添加" ➡️ "现有项" ➡️ 选择lib文件 ➡️ 确定
静态库的特点
优点:
函数库最终被打包到应用程序中,实现是函数本地化,寻址方便、速度快。(库函数调用效率和自定义函数使用效率基本相等)
程序在运行时与函数库再无瓜葛,移植方便。
缺点:
消耗系统资源较大,每个进程使用静态库都要复制一份,无端浪费内存。
静态库会给程序的更新、部署和发布带来麻烦。如果静态库libxxx.a
更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载)。
静态库的窘境
内存和磁盘空间
静态链接这种方法很简单,原理上也很容易理解,在操作系统和硬件不发达的早期,绝大部门系统采用这种方案。随着计算机软件的发展,这种方法的缺点很快暴露出来,那就是静态链接的方式对于计算机内存和磁盘空间浪费非常严重。特别是多进程操作系统下,静态链接极大的浪费了内存空间。在现在的linux系统中,一个普通程序会用到c语⾔静态库至少在1MB以上,那么如果磁盘中有2000个这样的程序,就要浪费将近2GB的磁盘空间。
程序开发和发布
空间浪费是静态链接的一个问题,另一个问题是静态链接对程序的更新、部署和发布也会带来很多麻烦。比如程序中所使用的mylib.lib
是由一个第三方⼚商提供的,当该厂商更新容量 mylib.lib
的时候,那么我们的程序就要拿到最新版的mylib.lib
,然后将其重新编译链接后,将新的程序整个发布给用户。这样的做缺点很明显,即一旦程序中有任何模块更新,整个程序 就要重新编译链接、发布给用户,用户要重新安装整个程序。
动态库/共享库
要解决空间浪费和更新困难这两个问题,最简单的办法就是把程序的模块相互分割开来,形成独⽴的文件,而不是将他们静态的链接在一起。简单地讲,就是不对哪些组成程序的目标程序进行链接,等程序运行的时候才进行链接。也就是说,把整个链接过程推迟到了运行时再进行,这就是动态链接的基本思想。
共享库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的拷贝,规避了空间浪费问题.动态库在程序运行时才被载入,也解决了静态库对程序的更新、部署和发布会带来麻烦。用户只需要更新动态库即可,增量更新。为什么需要动态库,其实也是静态库的特点导致。
在Linux环境中
按照习惯,一般以”.so”做为文件后缀名。共享库的命名一般分为三个部分:
前缀:
lib
库名称:自己定义即可,如
test
后缀:
.so
所以最终的静态库的名字应该为:libtest.so
创建动态库
生成动态链接库是直接使用gcc命令并且需要添加-fPIC
(-fpic
) 以及 -shared
参数。
-fPIC
或-fpic
参数的作用是使得 gcc 生成的代码是与位置无关的,也就是使用相对位置。-shared
参数的作用是告诉编译器生成一个动态链接库。
生成动态链接库的具体步骤如下:
将源文件进行汇编操作, 需要使用参数
-c
, 还需要添加额外参数-fpic
/-fPIC
shell# 得到若干个 .o文件 $ gcc 源文件(*.c) -c -fpic
将得到的
.o
文件打包成动态库, 还是使用gcc,使用参数-shared
指定生成动态库(位置没有要求)shell$ gcc -shared 与位置无关的目标文件(*.o) -o 动态库(libxxx.so)
发布动态库和头文件
提供头文件
**.h
提供制作出来的静态库
libxxx.so
使用动态库
引用动态库编译成可执行文件(跟静态库方式一样):
用到的参数:
-L
:指定要连接的库的所在目录-l
:指定链接时需要的动态库,去掉前缀和后缀-I
:指定main.c文件用到的头文件head.h所在的路径
gcc main.c -I./ -L./ -ltest2 -o main2
然后运行:./main2
,发现竟然报错了。
关于整个操作过程的报告:
gcc通过指定的动态库信息生成了可执行程序, 但是可执行程序运行却提示无法加载到动态库。
分析为什么在执行的时候找不到libtest2.so
库
- 当系统加载可执行代码时候,能够知道其所依赖的库的名字,但是还需要知道所依赖的库的绝对路径。此时就需要系统动态载入器(dynamic linker/loader)。
ldd命令可以查看可执行文件依赖的库文件,执行
ldd main2
,可以发现libtest2.so
找不到.
- 对于elf格式的可执行程序,是由
ld-linux.so*
来完成的,它先后搜索elf文件的 DT_RPATH段 — 环境变量LD_LIBRARY_PATH
—/etc/ld.so.cache
文件列表 —/lib/
,/usr/lib/
目录找到库文件后将其载入内存。
使用file命令可以查看文件的类型: file main2
让系统找到动态库
可执行程序生成之后, 根据动态链接器的搜索路径, 我们可以提供三种解决方案,我们只需要将动态库的路径放到对应的环境变量或者系统配置文件中,同样也可以将动态库拷贝到系统库目录(或者是将动态库的软链接文件放到这些系统库目录中)。
方案1: 将库路径添加到环境变量 LD_LIBRARY_PATH
中
找到相关的配置文件
- 用户级别:
~/.bashrc
—> 设置对当前用户有效 - 系统级别:
/etc/profile
—> 设置对所有用户有效
- 用户级别:
使用 vim 打开配置文件, 在文件最后添加这样一句话
shell# 自己把路径写进去就行了 export LIBRARY_PATH=$LIBRARY_PATH:动态库的绝对路径
让修改的配置文件生效
- 修改了用户级别的配置文件, 关闭当前终端, 打开一个新的终端配置就生效了
- 修改了系统级别的配置文件, 注销或关闭系统, 再开机配置就生效了
- 不想执行上边的操作, 可以执行一个命令让配置重新被加载
shell# 修改的是哪一个就执行对应的那个命令 # source 可以简写为一个 . , 作用是让文件内容被重新加载 $ source ~/.bashrc (. ~/.bashrc) $ source /etc/profile (. /etc/profile)
方案2: 更新 /etc/ld.so.cache
文件
找到动态库所在的绝对路径(不包括库的名字)比如:
/home/robin/Library/
使用vim 修改
/etc/ld.so.conf
这个文件, 将上边的路径添加到文件中(独自占一行)shell# 1. 打开文件 $ sudo vim /etc/ld.so.conf # 2. 添加动态库路径, 并保存退出
更新
/etc/ld.so.conf
中的数据到/etc/ld.so.cache
中shell# 必须使用管理员权限执行这个命令 $ sudo ldconfig
方案3: 拷贝动态库文件到系统库目录 /lib/
或者 /usr/lib
中 (或者将库的软链接文件放进去)
# 库拷贝
sudo cp /xxx/xxx/libxxx.so /usr/lib
# 创建软连接
sudo ln -s /xxx/xxx/libxxx.so /usr/lib/libxxx.so
之前的笔记
拷贝自己制作的共享库到
/lib
或者/usr/lib
临时设置
LD_LIBRARY_PATH
:export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
例如:
export LD_LIBRARY_PATH=/home/morax/code/makefile/test_lib_so/
查看是否设置成功
echo $LD_LIBRARY_PATH
,如果显示有值就证明设置成功了。
永久设置,把
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
,设置到∼/.bashrc
文件中,然后在执行下列三种办法之一:- 执行
. ~/.bashrc
使配置文件生效(第一个.
后面有一个空格) - 执行
source ~/.bashrc
配置文件生效 - 退出当前终端,然后再次登陆也可以使配置文件生效
⚠️
要添加的语句应为
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
,中间需要有$LD_LIBRARY_PATH
,意思是在原有的值后面拼接上添加的路径;如果没有$LD_LIBRARY_PATH
,相当于把原有的路径全都替换为现在设置的这一个路径了- 执行
永久设置,把
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:库路径
,设置到/etc/profile
文件中将其添加到
/etc/ld.so.cache
文件中编辑
/etc/ld.so.conf
文件,加入库文件所在目录的路径运行
sudo ldconfig -v
,该命令会重建/etc/ld.so.cache
文件
解决了库的路径问题之后,再次ldd命令可以查看可执行文件依赖的库文件,ldd main2:
在Windows环境中
参考链接 Microsoft演练:创建和使用自己的动态链接库 (C++)
创建动态库
- 创建一个新项目,在已安装的模板中选择“常规”,在右边的类型下选择“空项目”,在名称和 解决方案名称中输入mydll。点击确定。
- 在解决方案资源管理器的头文件中添加
mydll.h
文件,在源文件添加mydll.c
文件(即实现文 件)。 - 在
test.h
文件中添加如下代码:c#ifndef TEST_H #define TEST_H __declspec(dllexport) int myminus(int a, int b); #endif
- 在
test.c
文件中添加如下代码:c#include"test.h" int myminus(int a, int b){ return a - b; } // 实现时可以不写__declspec(dllexport)
- 配置项目属性。因为这是一个动态链接库,所以应在项目属性的“配置属性”下选择“常规”,在其下的配置类型中选择“动态库(
.dll
)。 - 编译生成新的解决方案,在Debug文件夹下会得到
mydll.dll
(对象文件库),将该.dll
文件、.lib
文件和相应头文件给用户,用户就可以使用该库里的函数了。
疑问一:__declspec(dllexport)
是什么意思?
动态链接库中定义有两种函数:导出函数(export function)和内部函数(internal function)。
导出函数可以被其它模块调用,内部函数在定义它们的DLL程序内部使用。
疑问二:动态库的lib文件和静态库的lib文件的区别?
在使用动态库的时候,往往提供两个文件:一个引入库(.lib
)文件(也称“导入库文件”)和一个DLL(.dll
)文件。虽然引入库的后缀名也是“lib”,但是,动态库的引入库文件和静态库文件有着本质的区别,对一个DLL文件来说,其引入库文件(.lib
)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。
使用动态库
方法一:隐式调用
创建主程序TestDll,将mydll.h
、mydll.dll
和mydll.lib
复制到源代码目录下。(P.S:头文件mydll.h
并不是必需的,只是C++中使用外部函数时,需要先进行声明)在程序中指定链接引用链接库 : #pragma comment(lib,"./mydll.lib"
)
方法二:显式调用
HANDLE hDll; //声明一个dll实例文件句柄
hDll = LoadLibrary("mydll.dll"); //导入动态链接库
MYFUNC minus_test; //创建函数指针
//获取导入函数的函数指针
minus_test = (MYFUNC)GetProcAddress(hDll, "myminus");
方法三:添加工程中
拷贝刚才生成的 .lib和头文件,到当前项目路径下,在项目属性中配置引入库。
- 右键项目 ➡ 属性 ➡ 链接器 ➡ 常规 ➡ 附加库目录 ➡ 添加上.lib库文件所在路径
- 右键项目 ➡ 属性 ➡ 链接器 ➡ 输入 ➡ 附加依赖项 ➡ 添加上.lib库文件的名称,带文件扩展名
如果头文件拷贝到了其他目录,需要在项目属性中配置附加包含目录
- 右键项目 ➡ 属性 ➡ C/C++ ➡ 常规 ➡ 附加包含目录 ➡ 添加库的头文件所在路径
拷贝 .dll 文件到当前项目的可执行程序生成路径下。
另外在使用函数时,需要在函数前加上 __declspec(dllimport)
,如下所示:
__declspec(dllimport) int myminus(int a, int b);
共享库的特点
动态库把对一些库函数的链接载入推迟到程序运行的时期。
可以实现进程之间的资源共享。(因此动态库也称为共享库)
将一些程序升级变得简单。
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)
比较动静态库的优缺点
静态库的优点:
- 执行速度快,是因为静态库已经编译到可执行文件内部了
- 移植方便,不依赖域其他的库文件
缺点:
- 耗费内存,是由于每一个静态库的可执行程序都会加载一次
- 部署更新麻烦,因为静态库修改以后所有的调用到这个静态库的可执行文件都需要重新编译
动态库的优点:
- 节省内存
- 部署升级更新方便,只需替换动态库即可,然后再重启服务
缺点:
- 加载速度比静态库慢
- 移植性差,需要把所有用到的动态库都移植
由于由静态库生成的可执行文件是把静态库加载到了其内部,所以静态库生成的可执行文件一般会比动态库大。