8、静态库与动态库:静态库(.a)的制作与使用、动态库(.so)的制作与使用
说到模块化编程,库文件是绕不开的话题。你想想看,我们辛辛苦苦写好的功能模块,总不能每次都把源码丢给同事编译吧?这时候就需要把代码打包成库。我个人习惯把库理解成「代码的压缩包」——别人能用你的功能,但看不到你的实现细节。
库分两种:静态库和动态库。名字听着玄乎,其实区别就一句话:静态库在编译时被塞进可执行文件,动态库在运行时才被加载。嗯,这里要注意,这个区别会直接影响你项目的体积、部署方式和维护成本。
核心区别速览:
- 静态库(.a / .lib):编译时链接,生成的可执行文件独立运行
- 动态库(.so / .dll):运行时加载,可执行文件依赖外部库文件
8.1 静态库的制作与使用
先说说静态库。我在项目中遇到过好几次这样的场景:一个底层驱动模块,客户要求提供功能但不给源码。静态库就是完美的解决方案。
8.1.1 制作静态库
制作过程其实就三步:写代码 → 编译成目标文件 → 打包成库。我习惯用 ar 命令来打包,它就像个文件归档工具。
假设我们有这么两个文件:
// math_ops.h
#ifndef MATH_OPS_H
#define MATH_OPS_H
int add(int a, int b);
int multiply(int a, int b);
#endif
// math_ops.c
#include "math_ops.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
制作静态库的命令如下:
# 第一步:编译成目标文件
gcc -c math_ops.c -o math_ops.o
# 第二步:打包成静态库
ar rcs libmath_ops.a math_ops.o
# 查看库中包含的目标文件
ar t libmath_ops.a
这里 ar rcs 的三个参数:r 表示插入或替换文件,c 表示创建库,s 表示生成索引。说白了,s 参数能让链接器更快地找到符号。
个人经验: 我建议在制作静态库时,把 .o 文件也保留一份。有一次客户说库有问题,我靠对比 .o 文件的编译选项才定位到问题——原来是优化等级不一致导致的。
8.1.2 使用静态库
使用静态库有两种方式,我分别说说:
// main.c
#include "math_ops.h"
#include <stdio.h>
int main() {
int result = add(3, 5);
printf("3 + 5 = %d\n", result);
return 0;
}
方式一:直接指定库文件路径
gcc main.c libmath_ops.a -o main
方式二:用 -L 和 -l 参数
gcc main.c -L. -lmath_ops -o main
注意 -lmath_ops 会自动补全为 libmath_ops.a。这是链接器的命名约定——库文件名必须是 libxxx.a 的格式。
我曾经踩过的坑: 静态库的链接顺序很重要!如果你有多个库互相依赖,被依赖的库要放在后面。比如 gcc main.c -lA -lB,如果 A 依赖 B,这样写没问题;但如果 B 依赖 A,就得写成 gcc main.c -lB -lA。这个坑我调试了整整一个下午才找到原因。
8.2 动态库的制作与使用
动态库就更有意思了。说白了,它是在程序启动时(或运行时)才被加载到内存。你想想看,如果多个程序都用同一个动态库,内存里只需要存一份代码——这就是共享库名字的由来。
8.2.1 制作动态库
制作动态库的关键是编译选项 -fPIC 和链接选项 -shared。
# 编译成位置无关的目标文件
gcc -fPIC -c math_ops.c -o math_ops.o
# 链接成动态库
gcc -shared -o libmath_ops.so math_ops.o
# 一步到位也可以
gcc -fPIC -shared -o libmath_ops.so math_ops.c
-fPIC 是什么意思?PIC 全称是 Position Independent Code(位置无关代码)。动态库加载到内存的地址是不固定的,所以代码里不能写死绝对地址。这个选项就是告诉编译器:生成可以放在任何地址的代码。
重要提醒: 动态库的命名有讲究。通常格式是 libxxx.so.major.minor,比如 libmath_ops.so.1.0.0。然后用软链接指向具体版本:
ln -s libmath_ops.so.1.0.0 libmath_ops.so
ln -s libmath_ops.so.1.0.0 libmath_ops.so.1
这样做的目的是:程序可以依赖 libmath_ops.so.1(主版本号),而具体实现可以升级到 1.0.1、1.0.2 而不影响程序运行。
8.2.2 使用动态库
编译时链接动态库和静态库的命令差不多:
gcc main.c -L. -lmath_ops -o main
但运行时有区别。动态库需要在运行时被找到,系统默认会去 /lib、/usr/lib 等标准路径查找。如果库在当前目录,你需要告诉系统去哪里找:
# 方法一:设置环境变量
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
# 方法二:编译时指定 rpath
gcc main.c -L. -lmath_ops -Wl,-rpath,. -o main
我个人更推荐用 -Wl,-rpath 的方式。因为 LD_LIBRARY_PATH 是运行时环境变量,万一用户没设置,程序就跑不起来。而 rpath 是编译时写进可执行文件的,更可靠。
8.2.3 动态库的加载与卸载
除了编译时链接,动态库还可以在程序运行时手动加载。这就要用到 dlopen 系列函数了:
#include <dlfcn.h>
#include <stdio.h>
int main() {
void *handle;
int (*add_func)(int, int);
char *error;
// 加载动态库
handle = dlopen("./libmath_ops.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "加载库失败: %s\n", dlerror());
return 1;
}
// 获取函数指针
add_func = dlsym(handle, "add");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "查找符号失败: %s\n", error);
dlclose(handle);
return 1;
}
// 调用函数
int result = add_func(3, 5);
printf("3 + 5 = %d\n", result);
// 卸载库
dlclose(handle);
return 0;
}
编译时需要链接 dl 库:
gcc main_dl.c -ldl -o main_dl
实际项目中的应用: 我在做一个插件系统时就用到了 dlopen。每个插件是一个动态库,主程序在启动时扫描插件目录,用 dlopen 加载所有 .so 文件。这样新增功能只需要放一个 .so 文件进去,完全不需要重新编译主程序。
8.3 静态库 vs 动态库:如何选择?
这个问题没有标准答案,我根据自己的经验整理了一个对比表:
| 对比维度 | 静态库 | 动态库 |
|---|---|---|
| 可执行文件大小 | 较大(包含库代码) | 较小(仅包含引用) |
| 部署复杂度 | 简单(单文件部署) | 复杂(需要附带 .so 文件) |
| 内存占用 | 每个进程一份 | 多进程共享一份 |
| 更新维护 | 需要重新编译可执行文件 | 替换 .so 文件即可 |
| 兼容性风险 | 低(代码已固化) | 高(动态库版本不匹配会出问题) |
| 启动速度 | 快(无需加载外部库) | 稍慢(需要加载和链接) |
我个人习惯这样选:如果是嵌入式系统、固件这类「烧进去就不动了」的场景,用静态库。如果是桌面应用、服务器软件这类需要频繁更新的场景,用动态库。
我曾经踩过的坑: 有一次升级了系统上的 OpenSSL 动态库,结果好几个依赖旧版本的应用全部崩溃。从那以后,我养成了一个习惯:给动态库做版本管理,用 soname 机制来保证兼容性。具体做法是在链接时加上 -Wl,-soname,libxxx.so.1,这样程序就只依赖主版本号,小版本升级不受影响。
8.4 本章知识体系
下面这张图总结了静态库和动态库的核心流程:
从这张图可以看得很清楚:静态库和动态库的起点是一样的——都是源码编译成目标文件。区别在于打包方式和链接时机。静态库用 ar 打包,在编译时直接嵌入可执行文件;动态库用 gcc -shared 生成,在运行时才加载。
嗯,到这里静态库和动态库的核心内容就讲完了。记住一句话:静态库是「复制粘贴」,动态库是「共享引用」。理解了这个本质,你就能根据实际场景做出合适的选择。
公众号:蓝海资料掘金营,微信 deep3321