第83章 动态库与静态库:如何创建和使用?

说实话,库这个话题,很多C语言学习者容易忽略。觉得不就是把代码打包一下嘛,有什么难的?

但我告诉你,我在嵌入式项目里吃过不少亏。有一次,一个静态库链接顺序搞错了,折腾了我整整一个下午。还有一次,动态库版本不兼容,现场升级直接翻车……嗯,从那以后,我对库的敬畏心就上来了。

今天咱们就把动态库和静态库彻底聊透。你想想看,搞懂了这个,以后写代码、做项目、甚至面试,都能硬气不少。

什么是库?为什么需要库?

库,说白了就是一堆目标文件(.o文件)的集合。把常用的函数打包成库,别人直接用就行,不用关心你怎么实现的。

举个例子:你写了一个加密算法,编译成库发给同事。同事只需要调用你的API,不需要看到你的源码。这就是库的核心价值——代码复用 + 接口隐藏

核心区别一句话:

  • 静态库(.a / .lib):编译时直接嵌入到可执行文件中。程序跑起来,跟库没关系了。
  • 动态库(.so / .dll):运行时才加载。程序启动后,再去内存里找库的代码。

静态库:创建与使用

先说说静态库。我个人习惯在项目早期用静态库,因为简单、省心,不用操心运行时依赖。

创建静态库

假设你有两个文件:math_ops.cmath_ops.h

// 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;
}

创建静态库的步骤很简单:

# 1. 编译成目标文件
gcc -c math_ops.c -o math_ops.o

# 2. 打包成静态库
ar rcs libmath_ops.a math_ops.o

# 3. 查看库里的符号(可选)
nm libmath_ops.a

ar rcs 这个命令,我刚开始用的时候老记不住。后来我就记住了:r = replace(替换),c = create(创建),s = index(索引)。嗯,这样就好记多了。

使用静态库

// 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 -L. -lmath_ops -o main_static

这里 -L. 表示在当前目录找库,-lmath_ops 表示链接 libmath_ops.a。注意,库名要去掉 lib 前缀和 .a 后缀。

小技巧: 链接静态库时,-l 选项要放在源文件后面。我曾经因为顺序写反了,链接报了一堆未定义错误。后来养成习惯:gcc main.c -L. -lmath_ops,源文件在前,库在后。

动态库:创建与使用

动态库就灵活多了。多个程序可以共享同一个动态库,节省内存。但代价是——你得管好运行时依赖。

创建动态库

还用刚才的 math_ops.c

# 编译成位置无关的目标文件
gcc -fPIC -c math_ops.c -o math_ops.o

# 创建动态库
gcc -shared -o libmath_ops.so math_ops.o

-fPIC 这个参数,我解释一下:PIC 是 Position Independent Code(位置无关代码)。动态库加载到内存的地址是不固定的,所以代码里不能写死地址。这个参数就是告诉编译器:生成位置无关的代码。

注意: 如果不加 -fPIC,在某些系统上(比如 x86_64)会报错。我在 ARM 平台上遇到过类似问题,当时没加这个参数,链接器直接罢工了。

使用动态库

# 编译链接动态库
gcc main.c -L. -lmath_ops -o main_dynamic

编译命令跟静态库一样。但运行的时候,问题就来了:

./main_dynamic
# 报错:cannot open shared object file: No such file or directory

为什么会这样?因为系统找不到 libmath_ops.so。你需要告诉系统去哪里找动态库。

解决方法有三种:

  1. 设置环境变量export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
  2. 把库拷贝到系统目录/usr/lib/usr/local/lib
  3. 编译时指定 rpathgcc main.c -L. -lmath_ops -Wl,-rpath,. -o main_dynamic

我个人推荐第三种方法。因为 LD_LIBRARY_PATH 容易搞乱环境,系统目录又需要 root 权限。用 -Wl,-rpath 把路径硬编码到可执行文件里,干净利落。

静态库 vs 动态库:怎么选?

这个问题,我在面试里被问过好几次。其实没有绝对的好坏,看场景。

对比维度 静态库 动态库
可执行文件大小 大(库代码被复制进来) 小(只记录引用)
运行时内存占用 每个进程一份 多个进程共享一份
更新维护 需要重新编译整个程序 只替换 .so 文件即可
部署复杂度 简单,一个文件搞定 需要管理依赖库
兼容性风险 低(代码已嵌入) 高(版本不匹配会崩溃)

我的经验是:

  • 嵌入式裸机开发:用静态库。因为系统简单,没有动态加载器。
  • Linux 应用开发:用动态库。方便更新,节省内存。
  • 第三方 SDK 发布:两种都给。静态库用于调试,动态库用于发布。

动态库的版本管理

这里我要重点说一下。动态库的版本问题,我曾经踩过大坑。

有一次,我更新了一个动态库,只是修复了一个小 bug,但接口没变。结果现场程序全部崩溃。后来发现,新库的某个结构体大小变了,旧程序还在按旧大小分配内存……

所以,动态库的版本号不是随便写的。Linux 下的命名规范是:

libxxx.so.major.minor.patch
  • major(主版本号):接口不兼容时增加。比如改了函数签名、结构体布局。
  • minor(次版本号):向后兼容的新功能。比如新增了一个函数。
  • patch(补丁号):bug 修复,接口完全不变。

编译时用 -Wl,-soname 指定 soname:

gcc -shared -Wl,-soname,libmath_ops.so.1 -o libmath_ops.so.1.0.0 math_ops.o

这样,链接器会记录 libmath_ops.so.1 这个 soname。以后升级到 1.0.1,只要 soname 还是 1,旧程序就能直接用。

一句话总结版本管理: 接口变了就升主版本,加功能就升次版本,修 bug 就升补丁号。别偷懒,否则线上翻车别怪我没提醒你。

知识体系总览

下面这张图,是我梳理的本章知识结构。你可以把它当成一个思维导图来看:

动态库与静态库知识体系 C语言库 静态库 (.a / .lib) 动态库 (.so / .dll) ar rcs 打包 编译时嵌入 无需运行时依赖 -fPIC 编译 运行时加载 版本管理 soname 机制 如何选择? 嵌入式裸机 → 静态库 Linux应用 → 动态库 图:动态库与静态库知识体系结构图

避坑指南

最后,分享几个我亲身踩过的坑:

  • 静态库链接顺序:如果库 A 依赖库 B,链接时 -lA -lB 的顺序不能反。否则会报未定义引用。我曾经在一个大型项目里,因为库依赖链太长,调顺序调了一上午。
  • 动态库的全局变量:多个动态库如果都定义了同名的全局变量,运行时可能会互相覆盖。我建议用 static 限制作用域,或者用命名前缀区分。
  • 不要混用不同编译器的库:GCC 和 ARMCC 编译出来的库,ABI(应用程序二进制接口)可能不兼容。混用的话,轻则崩溃,重则静默数据错误。
  • 动态库卸载:用 dlopen 加载的动态库,记得用 dlclose 卸载。否则会有内存泄漏。我在一个服务器程序里发现过这种问题,运行一周后内存暴涨。

最重要的提醒: 无论静态库还是动态库,接口文档一定要写清楚。我见过太多项目,库做出来了,但没人知道怎么用。最后大家各自重新造轮子……嗯,文档不是写给领导看的,是写给三个月后的自己看的。

好了,关于动态库和静态库,今天就聊到这里。记住:库是工具,不是目的。用对场景,才能事半功倍。


公众号:蓝海资料掘金营,微信 deep3321