C 语言动态链接库和静态链接库

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 内容如下:

1234567#ifndef MATH_UTILS_H#define MATH_UTILS_H// 函数原型,函数声明int add(int, int);#endif

math_utils.c 内容如下:

123456#include "math_utils.h"int add(int a, int b){ return a + b;}

在 main.c 中使用:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051/*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.orcs 参数含义: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 编译成 .ogcc -c add.c sub.c mul.c div.c# 2. 用 ar 一键打包所有的 .o 文件ar rcs libcalc.a *.o*/#include #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)

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

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

1ar rcs libmath.a math_utils.o

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

1234gcc main.c -L. -lmy_math -o main_static# orgcc main.c libmath.a -o main_static

-L. 表示在当前目录下查找库文件。

-lmath 表示链接名为 libmath.a 的库(自动补全前缀 lib 和后缀 .a)。

第四步:运行

1./main_static

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

1234ldd 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.h 和 math_utils.c 同上。

显式链接编写 main_explicit.c 如下:

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859// 程序运行时加载(显式链接)/* 1. 步骤一:将 math_utils.c 打包成 libmath.sogcc -fPIC -shared math_utils.c -o libmath.so-fPIC:生成“位置无关代码”(Position Independent Code),这是 .so 库必须的。-shared:告诉编译器生成一个共享库(动态库),而不是可执行文件。libmath.so:Linux 下动态库的标准命名格式为 lib[名字].so。2. 步骤二:编译主程序 main.cgcc main_explicit.c -o main -ldl-ldl:非常重要。这告诉链接器显式链接底层的 libdl 库,这样你的代码才能使用 dlopen、dlsym 等函数。3. 步骤三:运行程序./main_explicit*/#include#include#includeint 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)

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

-fPIC 告诉编译器生成“位置无关代码”(Position Independent Code),这是 .so 库必须的。这是动态库在内存中被多个进程共享的基础。

第二步:生成动态链接库

1gcc -shared math_utils.o -o libmath.so

-shared:告诉编译器生成一个共享库(动态库),而不是可执行文件。

libmath.so:Linux 下动态库的标准命名格式为 lib[名字].so。

第三步:编译主程序

1gcc main_explicit.c -o main_explicit -ldl

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

第四步:运行主程序

1./main_explicit

隐式链接math_utils.h 和 math_utils.c 同上,编写 main_implicit.c 如下:

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667/*隐式链接在编译时通过编译器和链接器将库与程序绑定,在程序启动时由操作系统自动加载 .so 文件。1. 步骤一:打包动态库gcc -fPIC -shared math_utils.c -o libmath.so2. 步骤二:编译并隐式链接主程序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 #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)

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

-fPIC 告诉编译器生成“位置无关代码”(Position Independent Code),这是 .so 库必须的。这是动态库在内存中被多个进程共享的基础。

第二步:生成动态链接库

1gcc -shared math_utils.o -o libmath.so

-shared:告诉编译器生成一个共享库(动态库),而不是可执行文件。

libmath.so:Linux 下动态库的标准命名格式为 lib[名字].so。

第三步:编译隐式链接主程序

1gcc main.c -o main -L. -lmath

-L.:告诉链接器在当前目录(.)寻找库文件。

-lmath:告诉链接器链接名为 libmath.so 的库。

(注:Linux 链接器会自动去掉 lib 前缀和 .so 后缀,所以写 math 即可)。

第四步:运行主程序

12./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 告诉操作系统运行时去哪里找库:

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

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

12gcc main_implicit.c -o main_implicit_fixpath -L. -lmath -Wl,-rpath=../main_implicit_fixpath

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

123sudo cp libmath.so /usr/local/lib/sudo ldconfig./main_implicit

查看执行程序链接了哪些动态库

1234567891011ldd 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 文件

升级维护

麻烦,库代码更新后,所有相关程序必须重新编译

方便,只需替换旧的动态库文件,程序无需重新编译即可更新

五、 我该如何选择?适合使用静态库的场景:

对部署便利性要求极高:如果你希望用户下载完你的单个可执行文件后开箱即用,不需要配置任何环境变量或额外安装依赖。

核心算法保护:不希望别人轻易地通过替换动态库来 Hook 或篡改你的功能。

适合使用动态库的场景:

大型项目或插件系统:当多个独立程序公用一套基础组件时(比如图形界面库 Qt、游戏引擎库),使用动态库能大幅减少磁盘和内存占用。

需要频繁更新业务逻辑:如果你的软件某些模块需要经常迭代升级,用动态库可以实现“无缝热更新”,用户甚至不需要重新下载主程序。

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

相关推荐