C 语言动态链接库和静态链接库
在 C 语言的开发过程中,我们经常会用到各种各样的库(Library)。简单来说,库就是已经编译好的、可复用的二进制代码。在 C 语言世界里,库主要分为两大类:静态链接库(Static Library)和动态链接库(Dynamic Library)。
很多初学者对这两者的区别感到模糊。本文将带你从底层原理、文件后缀、构建方法以及优缺点等维度,彻底搞懂这两个核心概念。
一、 编译的四个阶段回顾
在深入库之前,我们先快速复习一下一个 C 源文件(.c)变成可执行文件(.exe 或无后缀)的四个阶段:
- **预处理 (Preprocessing)**:处理
#include、#define等,生成.i文件。 - **编译 (Compilation)**:将代码翻译成汇编语言,生成
.s文件。 - **汇编 (Assembly)**:将汇编代码翻译成机器指令(二进制目标文件),生成
.o或.obj文件。 - 链接 (Linking):将多个
.o文件以及它们用到的库函数组装在一起,生成最终的可执行文件。
静态库和动态库的区别,就发生在第四阶段:链接阶段。
二、 什么是静态链接库?
1. 核心概念
静态链接库在编译链接阶段就会被直接拷贝到最终的可执行文件中。这意味着,一旦链接完成,可执行文件就包含了库中的所有必要代码,即便删除了原始的静态库文件,程序也能独立运行。
2. 文件后缀
- Linux / macOS:
.a(Archive) - Windows:
.lib
3. 生成与使用 (以 Linux GCC 为例)
假设我们有一个数学工具库 math_utils.h 和 math_utils.c,其中 math_utils.h 内容如下:
1 |
|
math_utils.c 内容如下:
1 |
|
在 main.c 中使用:
1 | /* |
第一步:将源文件编译为目标文件 (.o)
1 | gcc -c math_utils.c -o math_utils.o |
第二步:使用 ar 工具打包成静态库 (libmath.a)
1 | ar rcs libmath.a math_utils.o |
第三步:在主程序中链接并使用
1 | gcc main.c -L. -lmy_math -o main_static |
-L.表示在当前目录下查找库文件。-lmath表示链接名为libmath.a的库(自动补全前缀lib和后缀.a)。
第四步:运行
1 | ./main_static |
查看可执行程序 main_static 的动态链接库有哪些,不会包含 libmath.a:
1 | ldd main_static |
三、 什么是动态链接库?
1. 核心概念
动态链接库在链接阶段不会把代码复制到可执行文件中。相反,链接器只是在可执行文件中制作了一个“标记”或引用。当程序运行(Runtime)时,操作系统才会把动态库加载到内存中,供程序调用。
2. 文件后缀
- Linux:
.so(Shared Object) - macOS:
.dylib(Dynamic Library) - Windows:
.dll(Dynamic Link Library)
3. 生成与使用 (以 Linux GCC 为例)
使用动态链接库的方式有两种,一种是显式链接,一种是隐式链接。假设库代码 math_utils.h 和 math_utils.c 同上。
显式链接
编写 main_explicit.c 如下:
1 | // 程序运行时加载(显式链接) |
对于显式链接,可以使用下面的步骤进行编译、链接和运行程序。
第一步:编译为位置无关代码 (Position Independent Code)
1 | gcc -c -fPIC math_utils.c -o math_utils.o |
-fPIC告诉编译器生成“位置无关代码”(Position Independent Code),这是 .so 库必须的。这是动态库在内存中被多个进程共享的基础。
第二步:生成动态链接库
1 | gcc -shared math_utils.o -o libmath.so |
-shared:告诉编译器生成一个共享库(动态库),而不是可执行文件。libmath.so:Linux 下动态库的标准命名格式为 lib[名字].so。
第三步:编译主程序
1 | gcc main_explicit.c -o main_explicit -ldl |
-ldl:非常重要。这告诉链接器显式链接底层的 libdl 库,这样你的代码才能使用 dlopen、dlsym 等函数。
第四步:运行主程序
1 | ./main_explicit |
隐式链接
math_utils.h 和 math_utils.c 同上,编写 main_implicit.c 如下:
1 | /* |
对于隐式链接,可以使用下面的步骤进行编译、链接和运行程序。
第一步:编译为位置无关代码 (Position Independent Code)
1 | gcc -c -fPIC math_utils.c -o math_utils.o |
-fPIC告诉编译器生成“位置无关代码”(Position Independent Code),这是 .so 库必须的。这是动态库在内存中被多个进程共享的基础。
第二步:生成动态链接库
1 | gcc -shared math_utils.o -o libmath.so |
-shared:告诉编译器生成一个共享库(动态库),而不是可执行文件。libmath.so:Linux 下动态库的标准命名格式为 lib[名字].so。
第三步:编译隐式链接主程序
1 | gcc main.c -o main -L. -lmath |
-L.:告诉链接器在当前目录(.)寻找库文件。-lmath:告诉链接器链接名为 libmath.so 的库。(注:Linux 链接器会自动去掉 lib 前缀和 .so 后缀,所以写 math 即可)。
第四步:运行主程序
1 | ./main_implicit |
⚠️ 注意:此时运行
./main_explicit可能会报错说找不到.so文件。Linux 出于安全和性能考虑,程序运行时默认只去系统标准路径/lib或/usr/lib等系统目录下寻找动态库。不会在当前工作目录下寻找。方法 A:临时指定动态库路径(最推荐,适合开发调试)通过设置环境变量 LD_LIBRARY_PATH 告诉操作系统运行时去哪里找库:
1
2
3
4 export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main_implicit
或者下面这样,不影响环境变量,一次性:
LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main_implicit方法 B:在编译时将路径硬编码(适合本地项目)在编译时加入 -Wl,-rpath 参数,让程序记住动态库的相对路径。这样运行时就不需要配置任何环境变量,直接 ./main_implicit_fixpath 即可:
1
2 gcc main_implicit.c -o main_implicit_fixpath -L. -lmath -Wl,-rpath=.
./main_implicit_fixpath方法 C:将库移到系统目录(适合软件发布安装)将你的 .so 文件拷贝到系统的标准共享库目录中,并刷新系统库缓存:
1
2
3 sudo cp libmath.so /usr/local/lib/
sudo ldconfig
./main_implicit
查看执行程序链接了哪些动态库
1 | ldd main_implicit |
四、 静态库 vs 动态库
为了让你更直观地选择,我将它们的特点整理成了下表:
| 对比维度 | 静态链接库 (.a / .lib) |
动态链接库 (.so / .dll) |
|---|---|---|
| 链接时机 | 编译链接时,代码全量复制到程序中 | 程序运行时,才动态加载到内存中 |
| 文件大小 | 生成的可执行文件较大 | 生成的可执行文件较小 |
| 内存占用 | 多个程序运行同一份库时,每个程序内存里都有一份拷贝,浪费内存 | 多个程序可以共享内存中的同一份库代码,节省内存 |
| 独立性 | 极高,生成的可执行文件不需要任何外部依赖即可运行 | 较低,程序运行时必须能找到对应的 .so 或 .dll 文件 |
| 升级维护 | 麻烦,库代码更新后,所有相关程序必须重新编译 | 方便,只需替换旧的动态库文件,程序无需重新编译即可更新 |
五、 我该如何选择?
适合使用静态库的场景:
- 对部署便利性要求极高:如果你希望用户下载完你的单个可执行文件后开箱即用,不需要配置任何环境变量或额外安装依赖。
- 核心算法保护:不希望别人轻易地通过替换动态库来 Hook 或篡改你的功能。
适合使用动态库的场景:
- 大型项目或插件系统:当多个独立程序公用一套基础组件时(比如图形界面库 Qt、游戏引擎库),使用动态库能大幅减少磁盘和内存占用。
- 需要频繁更新业务逻辑:如果你的软件某些模块需要经常迭代升级,用动态库可以实现“无缝热更新”,用户甚至不需要重新下载主程序。
总结
静态链接是用空间换时间和稳定性,而动态链接则是用时间(运行时加载的微小开销)和复杂度换空间与灵活性。在现代软件开发中,由于内存和磁盘越来越便宜,动态链接库因其无与伦比的模块化和易维护性,成为了更主流的选择。







