第28章:静态库与动态库——.a、.so、.dll 的实战心法
说到库,很多初学者觉得不就是把代码打包一下嘛。嗯,这话没错,但打包的方式不同,后续的维护体验天差地别。我最早做嵌入式项目时,就吃过静态库版本混乱的亏,后来做 Linux 下的动态库,又踩过符号冲突的坑。今天我把这些经验掰开揉碎,跟你聊聊。
28.1 静态库(.a)的制作与使用
静态库,说白了就是把一堆 .o 目标文件打包成一个档案。链接时,链接器会把需要的代码直接拷贝到最终的可执行文件里。好处是部署简单,坏处是每个可执行文件都有一份副本,体积大,更新麻烦。
28.1.1 制作静态库
假设你有两个模块:math_ops.c 和 string_ops.c,以及对应的头文件。制作静态库只需要三步:
# 第一步:编译成目标文件
gcc -c math_ops.c -o math_ops.o
gcc -c string_ops.c -o string_ops.o
# 第二步:用 ar 打包成静态库
ar rcs libmyutils.a math_ops.o string_ops.o
# 第三步:查看库中的符号(可选)
nm libmyutils.a
ar 命令的 rcs 参数:r 表示插入或替换文件,c 表示创建库,s 表示生成索引。我个人习惯在 Makefile 里这样写:
libmyutils.a: math_ops.o string_ops.o
ar rcs $@ $^
28.1.2 使用静态库
使用静态库有两种方式:
# 方式一:直接链接 .a 文件
gcc main.c libmyutils.a -o main
# 方式二:用 -L 指定路径,-l 指定库名(去掉 lib 和 .a)
gcc main.c -L. -lmyutils -o main
28.2 动态库(.so / .dll)的制作与使用
动态库在链接时只记录符号引用,运行时才加载。这样多个程序可以共享同一份库代码,节省内存,更新也方便——换掉 .so 文件就行,不用重新编译所有程序。
28.2.1 Linux 下制作 .so
# 编译成位置无关的目标文件(-fPIC 必须加)
gcc -c -fPIC math_ops.c -o math_ops.o
gcc -c -fPIC string_ops.c -o string_ops.o
# 链接成动态库
gcc -shared -o libmyutils.so math_ops.o string_ops.o
# 一步到位
gcc -shared -fPIC -o libmyutils.so math_ops.c string_ops.c
-fPIC 这个参数,我曾经忘记加,结果链接时报了一堆重定位错误。记住:动态库必须用位置无关代码。
28.2.2 Windows 下制作 .dll
Windows 下的 DLL 制作稍微复杂一点,需要导出符号。有两种方式:
// 方式一:使用 __declspec(dllexport)
__declspec(dllexport) int add(int a, int b) {
return a + b;
}
// 方式二:使用 .def 文件(推荐用于大型项目)
// mylib.def 内容:
EXPORTS
add
subtract
编译命令:
cl /LD math_ops.c string_ops.c /Fe:myutils.dll
28.2.3 使用动态库
Linux 下使用动态库:
# 编译时链接
gcc main.c -L. -lmyutils -o main
# 运行时需要告诉系统库的位置
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
LD_LIBRARY_PATH 没设置,或者库没在标准路径(/usr/lib, /usr/local/lib)下。后来我习惯在 Makefile 里加一条 export LD_LIBRARY_PATH 的提示。
28.3 库的版本管理
版本管理是库维护中最容易被忽视的部分。我见过一个项目,因为动态库版本混乱,线上服务崩溃了整整两个小时。
28.3.1 语义化版本号
我推荐使用语义化版本号:主版本号.次版本号.修订号
| 版本变化 | 含义 | 示例 |
|---|---|---|
| 主版本号 | 不兼容的 API 修改 | 2.0.0 → 3.0.0 |
| 次版本号 | 向下兼容的功能新增 | 2.1.0 → 2.2.0 |
| 修订号 | 向下兼容的问题修正 | 2.1.0 → 2.1.1 |
28.3.2 Linux 动态库的版本命名
Linux 下动态库的版本命名有一套约定:
libfoo.so # 链接时使用的名称(软链接)
libfoo.so.1 # 主版本号 1 的 soname
libfoo.so.1.0.1 # 实际文件,完整版本号
创建方式:
# 编译时指定 soname
gcc -shared -fPIC -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.1 foo.c
# 创建软链接
ln -s libfoo.so.1.0.1 libfoo.so.1
ln -s libfoo.so.1 libfoo.so
soname 是动态库的身份证。程序运行时,系统根据 soname 来找库。我建议每次发布新版本时,都检查一下 soname 是否正确。
28.3.3 Windows DLL 版本管理
Windows 下用资源文件来管理版本:
// version.rc
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,1
PRODUCTVERSION 1,0,0,1
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "FileVersion", "1.0.0.1"
VALUE "ProductVersion", "1.0.0.1"
END
END
END
28.4 知识体系总览
下面这张图总结了静态库与动态库的核心知识脉络:
28.5 避坑指南与最佳实践
我总结的几个关键点:
- 静态库的链接顺序: 链接器从左到右处理库,被依赖的库要放在后面。我曾经因为顺序搞反,折腾了半天才发现是链接顺序问题。
- 动态库的符号冲突: 两个动态库导出同名函数,程序会调用哪个?答案是第一个加载的。为了避免这种问题,我习惯给库函数加独特的前缀,比如
myutils_add而不是add。 - 不要混用静态和动态的 C 运行时: 在 Windows 下尤其要注意,一个 DLL 用静态 CRT,另一个用动态 CRT,内存分配释放会出大问题。
ldd 查看可执行文件依赖了哪些动态库,用 nm -D 查看动态库导出了哪些符号。这两个命令能帮你快速定位 90% 的库相关问题。
嗯,关于库的制作和使用,今天就聊到这里。记住:静态库适合工具链和嵌入式场景,动态库适合桌面和服务端应用。版本管理不是可有可无的装饰,而是工程质量的基石。
公众号:蓝海资料掘金营,微信deep3321