C/C++动态库与静态库学习笔记
C/C++动态库与静态库
动态链接和静态链接是程序构建时处理库代码的两种不同方式。静态链接在编译阶段将库代码直接打包到最终的可执行文件中,生成的文件独立性强,但体积较大,且库更新时需要重新编译整个程序;而动态链接则是在程序运行时才加载所需的共享库(如Linux的.so或Windows的.dll文件),这种方式显著减小了可执行文件的体积,多个程序可以共享同一份库代码,节省内存,同时库的更新只需替换共享库文件即可生效,无需重新编译主程序。不过动态链接的运行时依赖需要确保库版本兼容,且启动时会有少量解析开销。静态链接适合嵌入式或需独立分发的场景,而动态链接更通用,尤其适合桌面和服务器应用,平衡了灵活性与资源效率。现代开发中默认优先动态链接,仅在特殊需求(如无外置依赖的部署)时选择静态链接。
不管是 Linux 还是 Windows 中的库文件其本质和工作模式都是相同的,只不过在不同的平台上库对应的文件格式和文件后缀不同。
静态库
定义: 静态库(Static Library),也称为归档文件(Archive),是一种包含多个已编译目标文件(.o
或 .obj
)的集合文件。常见的扩展名有 .a
(Linux/Unix)和 .lib
(Windows)。
目的: 提供一种方便的方式,将一组预先编译好的、相关的函数和代码模块打包在一起,供其他程序在编译链接阶段使用。
- 当你的程序(称为“主程序”)需要使用静态库中的函数或代码时,链接器(Linker) 会在编译的最后阶段(链接阶段)将静态库中被实际用到的目标文件的完整代码直接复制、合并到最终生成的可执行文件中。
- 最终的可执行文件是自包含的,它包含了它运行所需的所有代码,包括从静态库中复制过来的那部分。这意味着可执行文件不再依赖于原始的静态库文件来运行。
工作流程:
编写库代码: 开发者编写一组相关的函数或模块(例如,
math_functions.c
,string_utils.c
)。编译成目标文件: 使用编译器(如
gcc -c math_functions.c
)将每个源文件单独编译成目标文件(.o
)。创建静态库:
Linux/Unix: 使用
ar
(archiver) 工具:ar rcs libmymath.a math_functions.o string_utils.o
r
:替换或添加文件到归档。c
:创建一个库,不管库是否存在。s
:创建或更新归档的索引(非常重要,链接器需要索引来快速查找符号)。
Windows: 通常使用
lib.exe
工具:lib /OUT:mymath.lib math_functions.obj string_utils.obj
编写主程序: 开发者编写应用程序(
main.c
),并在其中调用静态库中提供的函数(例如,#include "math_functions.h"
然后调用add(5, 3)
)。编译主程序: 编译主程序的源文件成目标文件:
gcc -c main.c -o main.o
。链接:
- 使用链接器将主程序的目标文件(
main.o
)与静态库(libmymath.a
)链接起来生成最终的可执行文件(myapp
)。 - 命令示例:
gcc main.o -L. -lmymath -o myapp
-L.
:告诉链接器在当前目录(.
)查找库。-lmymath
:告诉链接器链接名为libmymath.a
的库(链接器会自动添加lib
前缀和.a
后缀)。
- 使用链接器将主程序的目标文件(
链接器的工作:
- 解析
main.o
中的符号(函数名、变量名)。 - 扫描指定的静态库
libmymath.a
。 - 从
libmymath.a
中找出并提取那些包含了main.o
所需符号(如add
函数)的具体目标文件(例如math_functions.o
)。 - 将这些目标文件的二进制代码直接复制到最终的可执行文件
myapp
中。 - 链接其他必要的库(如 C 标准库
libc.a
)。
- 解析
运行: 运行
./myapp
。此时,可执行文件myapp
自身已经包含了从静态库中复制过来的add
函数的代码,不再需要libmymath.a
文件。
静态库制作示例
测试程序目录结构如下:
.
├── add.c
├── div.c
├── include
│ └── head.h
├── main.c
├── mult.c
└── sub.c
编译成目标文件
$ gcc -c *.c
add.c:2:10: fatal error: head.h: 没有那个文件或目录
2 | #include "head.h"
| ^~~~~~~~
compilation terminated.
# 提示头文件找不到, 添加参数 -I 重新头文件路径即可
$ gcc add.c div.c mult.c sub.c -c -I ./include/
$ gcc -c *.c -I ./include/
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│ └── head.h
├── main.c
├── main.o
├── mult.c
├── mult.o
├── sub.c
└── sub.o
将生成的目标文件通过 ar
工具打包生成静态库
$ ar rcs libcalc.a *.o
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│ └── head.h
├── libcalc.a
├── main.c
├── main.o
├── mult.c
├── mult.o
├── sub.c
└── sub.o
然后将生成的的静态库 libcalc.a和库对应的头文件head.h一并发布给使用者就可以了。
静态库的使用
当我们得到了一个可用的静态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对静态库中的函数进行调用。
# 1. 首先拿到了发布的静态库
`head.h` 和 `libcalc.a`
# 2. 将静态库, 头文件, 测试程序放到一个目录中准备进行测试
.
├── head.h # 函数声明
├── libcalc.a # 函数定义(二进制格式)
└── main.c # 函数测试
编译测试程序, 得到可执行文件。
上述错误分析:
编译的源文件中包含了头文件
head.h
, 这个头文件中声明的函数对应的定义(也就是函数体实现)在静态库中,程序在编译的时候没有找到函数实现,因此提示 undefined reference to xxxx。解决方案:在编译的时将静态库的路径和名字都指定出来
-L
: 指定库所在的目录(相对或者绝对路径)-l
: 指定库的名字, 需要掐头(lib)去尾(.a) 剩下的才是需要的静态库的名字
gcc main.c -o app -L ./ -l calc
动态库
定义: 动态库是一种包含编译好的代码和数据的文件,可以在程序运行时被加载到内存中,并被多个正在运行的程序共享使用。
动态链接库是程序运行时加载的库,当动态链接库正确部署之后,运行的多个程序可以使用同一个加载到内存中的动态库,因此在Linux中动态链接库也可称之为共享库。
常见的扩展名有:
- Linux:
libxxx.so
(Shared Object) - Windows:
libxxx.dll
(Dynamic Link Library)
目的: 提供通用的功能模块,允许程序在运行时(而非编译链接时)加载和使用这些功能,实现代码复用、节省资源和方便更新。
关键特性:“动态链接”
- 当程序需要使用动态库中的函数时,编译链接阶段并不会将库代码复制到最终的可执行文件中。
- 链接器(Linker)在编译链接阶段只会在可执行文件中记录它依赖哪些动态库以及需要调用库中的哪些符号(函数、变量名)。
- 在程序启动时(加载时链接) 或运行过程中(运行时链接),由操作系统的动态链接器/加载器负责将所需的动态库加载到内存(如果尚未加载),并解析可执行文件中记录的符号引用,将其绑定(链接) 到内存中动态库的实际代码地址上。
- 最终的可执行文件不包含动态库的代码本身,它依赖于外部的动态库文件才能正确运行。
工作流程:
- 编写库代码: 开发者编写一组相关的函数或模块(例如,
math_functions.c
,string_utils.c
)。 - 编译成位置无关代码 (PIC):
- 这是创建动态库的关键一步:
gcc -c -fPIC math_functions.c
-fPIC
(Position Independent Code):指示编译器生成位置无关代码。这种代码无论被加载到内存的哪个地址,都能正确执行(因为它不依赖绝对地址,而是使用相对偏移或全局偏移表 GOT)。这是实现多个进程共享同一份库代码物理内存页的基础。
- 这是创建动态库的关键一步:
- 创建动态库:
- Linux/Unix:
gcc -shared *.o -o libmymath.so
-shared
:告诉链接器生成一个共享对象(动态库)。
- Windows (VC++): 编译器和链接器设置通常会自动处理。大致过程:将源代码编译为目标文件(
.obj
),然后使用链接器 (link.exe
) 指定/DLL
选项:link /DLL /OUT:mymath.dll math_functions.obj string_utils.obj
- Linux/Unix:
- 编写主程序: 开发者编写应用程序(
main.c
),调用动态库中的函数(#include "math_functions.h"
)。 - 编译主程序:
gcc -c main.c -o main.o
- 链接主程序:
- 使用链接器将主程序的目标文件(
main.o
)与动态库的导入信息链接起来,生成最终的可执行文件(myapp
)。 - 命令示例:
gcc main.o -L . -lmymath -o myapp
-L.
:告诉链接器在当前目录查找库。-lmymath
:告诉链接器程序依赖名为libmymath.so
的动态库(链接器会自动添加lib
前缀和.so
后缀)。
- 链接器的工作(编译时):
- 解析
main.o
中的符号。 - 确认这些符号在动态库
libmymath.so
的导出符号表中。 - 不会将
libmymath.so
的代码复制到myapp
中! - 在
myapp
文件中记录:- 它依赖
libmymath.so
。 - 它需要
libmymath.so
中的哪些符号(如add
)。 - 这些符号在
myapp
内部的占位地址(通常是.plt
表中的条目)。
- 它依赖
- 解析
- 使用链接器将主程序的目标文件(
- 运行程序(加载时动态链接):
- 用户输入
./myapp
。 - 操作系统加载器读取
myapp
文件,发现它依赖libmymath.so
。 - 操作系统加载器/动态链接器 (
ld.so
on Linux,ldd
可查看依赖):- 搜索
libmymath.so
文件(按照标准路径如/usr/lib
,/lib
或LD_LIBRARY_PATH
环境变量指定的路径)。 - 如果找到,将其加载到内存。如果该库已被其他程序加载,操作系统通常会尝试共享其代码段的物理内存页(节省内存),但数据段通常是进程私有的(Copy-on-Write)。
- 执行库的初始化代码(如果有,如
_init
或构造函数)。 - 解析符号: 查找
myapp
中记录的所需符号(add
)在内存中libmymath.so
的实际地址。 - 重定位: 将
myapp
中指向add
的占位地址(.plt
条目)修正(绑定) 为内存中libmymath.so
里add
函数的真实地址。
- 搜索
- 程序
myapp
开始执行。当它调用add(5, 3)
时,CPU 跳转到内存中libmymath.so
代码段里的add
函数地址执行。
- 用户输入
- 运行时动态链接 (可选):
- 程序可以在运行期间,根据需要,显式地加载动态库、查找符号并调用函数。这提供了更大的灵活性(如插件系统)。
- Linux/Unix: 使用
dlopen()
,dlsym()
,dlclose()
,dlerror()
函数。 - Windows: 使用
LoadLibrary()
,GetProcAddress()
,FreeLibrary()
函数。
动态库制作举例
使用gcc
将源文件进行汇编(参数-c
), 生成与位置无关的目标文件, 需要使用参数 -fpic
或者-fPIC
$ gcc -c -fpic *.c -I include/
$ tree
.
├── add.c
├── add.o
├── div.c
├── div.o
├── include
│ └── head.h
├── main.c
├── main.o
├── mult.c
├── mult.o
├── sub.c
├── sub.o
└── test
使用gcc
将得到的目标文件打包生成动态库, 需要使用参数 -shared
gcc -shared *.o -o libcalc.so
发布生成的动态库和相关的头文件
# 3. 发布库文件和头文件
1. head.h
2. libcalc.so
动态库的使用
当我们得到了一个可用的动态库之后, 需要将其放到一个目录中, 然后根据得到的头文件编写测试代码, 对动态库中的函数进行调用。
# 1. 拿到发布的动态库
`head.h libcalc.so
# 2. 基于头文件编写测试程序, 测试动态库中提供的接口是否可用
`main.c`
# 示例目录:
.
├── head.h ==> 函数声明
├── libcalc.so ==> 函数定义
└── main.c ==> 函数测试
和使用静态库一样, 在编译的时候需要指定库相关的信息: 库的路径
-L
和 库的名字-l
gcc main.c -o app -L./ -lcalc
然而,执行完这一步,运行可执行文件却会出现如下报错:
执行生成的可执行程序, 错误提示 ==> 可执行程序执行的时候找不到动态库
$ ./app
./app: error while loading shared libraries: libcalc.so: cannot open shared object file: No such file or directory
解决方法如下:
解决动态库无法加载的问题
库的工作原理
静态库如何被加载
在程序编译的最后一个阶段也就是链接阶段,提供的静态库会被打包到可执行程序中。当可执行程序被执行,静态库中的代码也会一并被加载到内存中,因此不会出现静态库找不到无法被加载的问题。
动态库如何被加载
在程序编译的最后一个阶段也就是链接阶段:
在gcc命令中虽然指定了库路径(使用参数 -L ), 但是这个路径并没有记录到可执行程序中,只是检查了这个路径下的库文件是否存在。
同样对应的动态库文件也没有被打包到可执行程序中,只是在可执行程序中记录了库的名字
可执行程序被执行起来之后:
程序执行的时候会先检测需要的动态库是否可以被加载,加载不到就会提示上边的错误信息
当动态库中的函数在程序中被调用了, 这个时候动态库才加载到内存,如果不被调用就不加载
动态库的检测和内存加载操作都是由动态连接器来完成的
动态链接器
动态链接器是一个独立于应用程序的进程, 属于操作系统, 当用户的程序需要加载动态库的时候动态连接器就开始工作了,很显然动态连接器根本就不知道用户通过 gcc 编译程序的时候通过参数 -L指定的路径。
动态链接器按以下顺序查找库文件:
- 可执行文件本身记录的路径(通过
-rpath
或-RPATH
编译选项指定) - 环境变量
LD_LIBRARY_PATH
指定的路径 - 配置文件
/etc/ld.so.conf
及缓存/etc/ld.so.cache
- 系统默认路径(如
/lib
、/usr/lib
)
按照以上四个顺序, 依次搜索, 找到之后结束遍历, 最终还是没找到, 动态连接器就会提示动态库找不到的错误信息。
解决方案
方案1:将库路径添加到环境变量 LD_LIBRARY_PATH 中
找到相关的配置文件
- 用户级别:·
~/.bashrc
—> 设置对当前用户有效 - 系统级别:
/etc/profile
—> 设置对所有用户有效
使用 vim 打开配置文件, 在文件最后添加这样一句话
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH :动态库的绝对路径
让修改的配置文件生效
- 修改了用户级别的配置文件, 关闭当前终端, 打开一个新的终端配置就生效了
- 修改了系统级别的配置文件, 注销或关闭系统, 再开机配置就生效了
- 不想执行上边的操作, 可以执行一个命令让配置重新被加载
# 修改的是哪一个就执行对应的那个命令 # source 可以简写为一个 . , 作用是让文件内容被重新加载 $ source ~/.bashrc (. ~/.bashrc) $ source /etc/profile (. /etc/profile)
- 用户级别:·
方案2: 更新 /etc/ld.so.cache 文件
找到动态库所在的绝对路径(不包括库的名字)比如:/home/workspace/Library/
使用vim 修改 /etc/ld.so.conf 这个文件, 将上边的路径添加到文件中(独自占一行)
# 1. 打开文件 $ sudo vim /etc/ld.so.conf # 2. 添加动态库路径, 并保存退出
更新
/etc/ld.so.conf
中的数据到/etc/ld.so.cache
中# 必须使用管理员权限执行这个命令 $ 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
验证
在启动可执行程序之前, 或者在设置了动态库路径之后, 我们可以通过一个命令检测程序能不能够通过动态链接器加载到对应的动态库, 这个命令叫做 ldd
# 语法:
$ ldd 可执行程序名
# 举例:
$ ldd app
linux-vdso.so.1 (0x00007c49d17d8000)
libcalc.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007c49d1400000)
/lib64/ld-linux-x86-64.so.2 (0x00007c49d17da000)