在 C 语言的开发过程中,我们经常会用到各种各样的库(Library)。简单来说,库就是已经编译好的、可复用的二进制代码。在 C 语言世界里,库主要分为两大类:静态链接库(Static Library)动态链接库(Dynamic Library)

很多初学者对这两者的区别感到模糊。本文将带你从底层原理、文件后缀、构建方法以及优缺点等维度,彻底搞懂这两个核心概念。

一、 编译的四个阶段回顾

在深入库之前,我们先快速复习一下一个 C 源文件(.c)变成可执行文件(.exe 或无后缀)的四个阶段:

  1. **预处理 (Preprocessing)**:处理 #include#define 等,生成 .i 文件。
  2. **编译 (Compilation)**:将代码翻译成汇编语言,生成 .s 文件。
  3. **汇编 (Assembly)**:将汇编代码翻译成机器指令(二进制目标文件),生成 .o.obj 文件。
  4. 链接 (Linking):将多个 .o 文件以及它们用到的库函数组装在一起,生成最终的可执行文件

静态库和动态库的区别,就发生在第四阶段:链接阶段。


二、 什么是静态链接库?

1. 核心概念

静态链接库在编译链接阶段就会被直接拷贝到最终的可执行文件中。这意味着,一旦链接完成,可执行文件就包含了库中的所有必要代码,即便删除了原始的静态库文件,程序也能独立运行。

2. 文件后缀

  • Linux / macOS: .a (Archive)
  • Windows: .lib

3. 生成与使用 (以 Linux GCC 为例)

假设我们有一个数学工具库 math_utils.hmath_utils.c,其中 math_utils.h 内容如下:

1
2
3
4
5
6
7
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

// 函数原型,函数声明
int add(int, int);

#endif

math_utils.c 内容如下:

1
2
3
4
5
6
#include "math_utils.h"

int add(int a, int b)
{
return a + b;
}

main.c 中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/*
1. 将 math_utils.c 打包成静态库 libmath.a在 Linux 中,打包静态库不使用 gcc -shared,而是使用 gcc 先生成中间目标文件,再使用系统自带的归档工具 ar。

# 1. 先将源码编译为 .o 目标文件(不需要 -fPIC)
gcc -c math_utils.c -o math_utils.o

# 2. 使用 ar 工具将 .o 文件打包归档为 .a 静态库
ar rcs libmath.a math_utils.o

rcs 参数含义:r 代替已有文件,c 创建库,s 建立符号表索引以加快链接。
命名规范:Linux 静态库同样必须以 lib 开头,.a 结尾。
如果有多个 .o 文件,可以这样:ar rcs libmath.a math_utils.o text_utils.o
查看静态库中包含哪些 .o 文件: ar t libmath.a
提取(解压)静态库中的 .o 文件: ar x libmath.a
从静态库中删除某个模块: ar d libmath.a text_utils.o
配套高级命令:nm(查看库里的函数), 由于 ar t 只能看到 .o 文件名,看不到里面具体有什么函数。
如果你想知道静态库里到底有哪些函数接口可以调用,通常会配合 nm 命令使用:
nm libmath.a

math_utils.o:
0000000000000000 T add
(注:T 代表该函数(如 add)在代码区中,是可以被外部程序调用的)。

2. 编译并链接静态库静态库的使用完全是隐式链接的。编译 main.c 时,我们把 libmath.a 直接喂给编译器:
gcc main.c -o main_static -L. -lmath

-L. 和 -lmath 的用法与动态库完全一致,链接器会自动优先寻找 .so,如果找不到,或者你显式指定,它就会去链接 libmath.a。
提示:你也可以更粗暴地直接指定路径:gcc main.c libmath.a -o main_static


扩展:
多文件打包示例假设你开发了一个大项目,里面有加减乘除四个文件:add.c、sub.c、mul.c、div.c。
你可以用以下两行命令一步到位打包:
# 1. 把所有 .c 编译成 .o
gcc -c add.c sub.c mul.c div.c

# 2. 用 ar 一键打包所有的 .o 文件
ar rcs libcalc.a *.o

*/

#include <stdio.h>
#include "math_utils.h"

int main(int argc, const char *argv[])
{

int result = add(10, 30);
printf("静态库链接:10 + 30 = %d\n", result);
return 0;
}

第一步:将源文件编译为目标文件 (.o)

1
gcc -c math_utils.c -o math_utils.o

第二步:使用 ar 工具打包成静态库 (libmath.a)

1
ar rcs libmath.a math_utils.o

第三步:在主程序中链接并使用

1
2
3
4
gcc main.c -L. -lmy_math -o main_static

# or
gcc main.c libmath.a -o main_static
  • -L. 表示在当前目录下查找库文件。
  • -lmath 表示链接名为 libmath.a 的库(自动补全前缀 lib 和后缀 .a)。

第四步:运行

1
./main_static

查看可执行程序 main_static 的动态链接库有哪些,不会包含 libmath.a:

1
2
3
4
ldd main_static 
linux-vdso.so.1 (0x00007ffe5c1e1000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f716f4f5000)
/lib64/ld-linux-x86-64.so.2 (0x00007f716f746000)

三、 什么是动态链接库?

1. 核心概念

动态链接库在链接阶段不会把代码复制到可执行文件中。相反,链接器只是在可执行文件中制作了一个“标记”或引用。当程序运行(Runtime)时,操作系统才会把动态库加载到内存中,供程序调用。

2. 文件后缀

  • Linux: .so (Shared Object)
  • macOS: .dylib (Dynamic Library)
  • Windows: .dll (Dynamic Link Library)

3. 生成与使用 (以 Linux GCC 为例)

使用动态链接库的方式有两种,一种是显式链接,一种是隐式链接。假设库代码 math_utils.hmath_utils.c 同上。

显式链接

编写 main_explicit.c 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 程序运行时加载(显式链接)

/*
1. 步骤一:将 math_utils.c 打包成 libmath.so
gcc -fPIC -shared math_utils.c -o libmath.so

-fPIC:生成“位置无关代码”(Position Independent Code),这是 .so 库必须的。
-shared:告诉编译器生成一个共享库(动态库),而不是可执行文件。
libmath.so:Linux 下动态库的标准命名格式为 lib[名字].so。

2. 步骤二:编译主程序 main.c
gcc main_explicit.c -o main -ldl

-ldl:非常重要。这告诉链接器显式链接底层的 libdl 库,这样你的代码才能使用 dlopen、dlsym 等函数。

3. 步骤三:运行程序
./main_explicit

*/

#include<stdio.h>
#include<stdlib.h>
#include<dlfcn.h>

int main(int argc, const char *argv[])
{
void *handle;
int (*add_func)(int, int); // 定义一个函数指针接收 add 函数
char *error;

// 1. 运行时动态加载链接库
// ./libmath.so 是动态链接库的路径,RTLD_LAZY 表示懒加载(用到时才解析符号)
handle = dlopen("./libmath.so", RTLD_LAZY);
if (!handle)
{
fprintf(stderr, "加载动态库失败:%s\n", dlerror());
exit(EXIT_FAILURE);
}

// 清楚现有的错误
dlerror();

// 2. 从动态库中查找 add 函数的地址
*(void **)(&add_func) = dlsym(handle, "add");
if((error = dlerror()) != NULL) {
fprintf(stderr, "找不到函数:%s\n", error);
dlclose(handle);
exit(EXIT_FAILURE);
}

// 3. 通过函数指针调用动态库中的函数
int result = add_func(10, 30);
printf("动态库调用成功:10 + 30 = %d\n", result);

// 4. 关闭动态链接库
dlclose(handle);

return 0;
}

对于显式链接,可以使用下面的步骤进行编译、链接和运行程序。

第一步:编译为位置无关代码 (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.hmath_utils.c 同上,编写 main_implicit.c 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
/*
隐式链接在编译时通过编译器和链接器将库与程序绑定,在程序启动时由操作系统自动加载 .so 文件。

1. 步骤一:打包动态库
gcc -fPIC -shared math_utils.c -o libmath.so

2. 步骤二:编译并隐式链接主程序
gcc main.c -o main -L. -lmath

-L.:告诉链接器在当前目录(.)寻找库文件。
-lmath:告诉链接器链接名为 libmath.so 的库。
(注:Linux 链接器会自动去掉 lib 前缀和 .so 后缀,所以写 math 即可)。

3. 步骤三:运行
./main_implicit
./main_implicit: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory

原因:Linux 出于安全和性能考虑,程序运行时默认只去系统标准路径(如 /lib, /usr/lib)寻找 .so 文件,不会在当前工作目录下寻找。
你需要使用以下 三种方法之一 来成功运行它:
方法 A:临时指定动态库路径(最推荐,适合开发调试)通过设置环境变量 LD_LIBRARY_PATH 告诉操作系统运行时去哪里找库:

export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main_implicit

下次使用时就直接运行程序即可:./main_implicit

或者下面这样,不影响环境变量,一次性:
LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./main_implicit

方法 B:在编译时将路径硬编码(适合本地项目)在编译时加入 -Wl,-rpath 参数,让程序记住动态库的相对路径。
这样运行时就不需要配置任何环境变量,直接 ./main_implicit_fixpath 即可:
gcc main_implicit.c -o main_implicit_fixpath -L. -lmath -Wl,-rpath=.
./main_implicit_fixpath

方法 C:将库移到系统目录(适合软件发布安装)将你的 .so 文件拷贝到系统的标准共享库目录中,并刷新系统库缓存:
sudo cp libmath.so /usr/local/lib/
sudo ldconfig
./main_implicit


查询依赖的静态链接库
ldd main_implicit
linux-vdso.so.1 (0x00007ffc8ba45000)
libmath.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ee8a1400000)
/lib64/ld-linux-x86-64.so.2 (0x00007ee8a1642000)


ldd main_implicit_fixpath
linux-vdso.so.1 (0x00007ffd8b797000)
libmath.so => ./libmath.so (0x0000710b228f8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000710b22600000)
/lib64/ld-linux-x86-64.so.2 (0x0000710b22904000)
*/

#include <stdio.h>
#include "math_utils.h"

int main(int argc, const char *argv[])
{
// 像调用标准库函数一样,直接调用 add
int result = add(10, 30);

printf("隐式调用链接库成功: 10 + 30 = %d\n", result);

return 0;
}

对于隐式链接,可以使用下面的步骤进行编译、链接和运行程序。

第一步:编译为位置无关代码 (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
2
./main_implicit
./main_implicit: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory

⚠️ 注意:此时运行 ./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
2
3
4
5
6
7
8
9
10
11
ldd main_implicit
linux-vdso.so.1 (0x00007ffc8ba45000)
libmath.so => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ee8a1400000)
/lib64/ld-linux-x86-64.so.2 (0x00007ee8a1642000)

ldd main_implicit_fixpath
linux-vdso.so.1 (0x00007ffd8b797000)
libmath.so => ./libmath.so (0x0000710b228f8000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000710b22600000)
/lib64/ld-linux-x86-64.so.2 (0x0000710b22904000)

四、 静态库 vs 动态库

为了让你更直观地选择,我将它们的特点整理成了下表:

对比维度 静态链接库 (.a / .lib) 动态链接库 (.so / .dll)
链接时机 编译链接时,代码全量复制到程序中 程序运行时,才动态加载到内存中
文件大小 生成的可执行文件较大 生成的可执行文件较小
内存占用 多个程序运行同一份库时,每个程序内存里都有一份拷贝,浪费内存 多个程序可以共享内存中的同一份库代码,节省内存
独立性 极高,生成的可执行文件不需要任何外部依赖即可运行 较低,程序运行时必须能找到对应的 .so.dll 文件
升级维护 麻烦,库代码更新后,所有相关程序必须重新编译 方便,只需替换旧的动态库文件,程序无需重新编译即可更新

五、 我该如何选择?

适合使用静态库的场景:

  1. 对部署便利性要求极高:如果你希望用户下载完你的单个可执行文件后开箱即用,不需要配置任何环境变量或额外安装依赖。
  2. 核心算法保护:不希望别人轻易地通过替换动态库来 Hook 或篡改你的功能。

适合使用动态库的场景:

  1. 大型项目或插件系统:当多个独立程序公用一套基础组件时(比如图形界面库 Qt、游戏引擎库),使用动态库能大幅减少磁盘和内存占用。
  2. 需要频繁更新业务逻辑:如果你的软件某些模块需要经常迭代升级,用动态库可以实现“无缝热更新”,用户甚至不需要重新下载主程序。

总结

静态链接是用空间换时间稳定性,而动态链接则是用时间(运行时加载的微小开销)和复杂度换空间灵活性。在现代软件开发中,由于内存和磁盘越来越便宜,动态链接库因其无与伦比的模块化和易维护性,成为了更主流的选择。