【Linux:文件】Linux 动静态库详解::制作、使用、原理与实战
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux操作系统从入门到实践》《Qt从入门到实践》
《算法题讲解指南》--优选算法
《算法题讲解指南》--递归、搜索与回溯算法
《算法题讲解指南》--动态规划算法
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
前言
一. 库的基础认知:是什么?有哪些?
1.1 库的本质:是什么?
1.2 库的分类与系统位置:有哪些
1.3 编译器链接行为
1.4 预备工作:自定义库源码
二、静态库(编译时链接,独立运行)
2.1 整体图示
2.2 静态库制作流程(Makefile自动化 ,更简便)
2.2.1 编写 Makefile
2.2.2 查看静态库内容
2.3 静态库使用场景与命令
2.4 静态库核心特点
三. 动态库(运行时链接,共享复用)
3.1 动态库制作流程(Makefile自动化)
3.1.1 编写 Makefile
3.2 动态库使用:编译与运行时依赖
3.2.1 问题现象
3.2.2 解决方案
3.3 动态库核心特点
四. 动静态库对比与选型建议
五、实战:使用外部库(ncurses 图形库)
5.1 安装 ncurses 库
5.2. 测试代码(大家可以自己试试别的)
结束语
前言
在 Linux 开发中,库是实现代码复用的核心方式。无论是 C 标准库提供的 printf,还是我们自行封装的工具函数,都能编译为库文件,供多个项目直接调用。库主要分为静态库(.a)和动态库(.so),二者在编译链接、内存占用、部署更新等方面存在显著差异。熟练掌握库的制作、使用及底层原理,是 Linux 开发的必备能力。
一. 库的基础认知:是什么?有哪些?
1.1 库的本质:是什么?
库是编译后的二进制文件,包含可复用的代码和数据,本质是 “提前写好、经过验证的成熟代码”。其核心价值在于:
- 避免重复开发:无需从零实现基础功能(如字符串处理、文件 IO);
- 简化项目管理:将复杂功能拆分到库中,降低主项目复杂度;
- 隐藏实现细节:只暴露接口,保护核心逻辑。
1.2 库的分类与系统位置:有哪些
Linux 下库分为两类,命名和存储路径有明确规范:
| 类型 | 后缀 (Linux) | 后缀 (Windows) | 系统默认路径 | 核心特征 |
|---|---|---|---|---|
| 静态库 | .a | .lib | /lib,/usr/lib,/usr/local/lib | 编译时链接,可执行程序独立运行 |
| 动态库 | .so | .dll | /lib64,/usr/lib64,/usr/local/lib64 | 运行时链接,多程序共享 |
系统中的库示例:
Ubuntu 系统
C 标准库:
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so # 动态库 -rwxr-xr-x 1 root root 2029592 May 1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so $ ls -l /lib/x86_64-linux-gnu/libc.a # 静态库 -rw-r--r-- 1 root root 5747594 May 1 02:20 /lib/x86_64-linux-gnu/libc.aC++ 标准库:
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -l # 动态库 lrwxrwxrwx 1 root root 40 Oct 24 2022 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so -> ../../../x86_64-linux-gnu/libstdc++.so.6 $ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a # 静态库 /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.aCentOS 系统
C 标准库:
$ ls /lib64/libc-2.17.so -l # 动态库 -rwxr-xr-x 1 root root 2156592 Jun 4 23:05 /lib64/libc-2.17.so $ ls /lib64/libc.a -l # 静态库 -rw-r--r-- 1 root root 5105516 Jun 4 23:05 /lib64/libc.aC++ 标准库:
$ ls /lib64/libstdc++.so.6 -l # 动态库(软链接到实际版本) lrwxrwxrwx 1 root root 19 Sep 18 20:59 /lib64/libstdc++.so.6 -> libstdc++.so.6.0.19 $ ls /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a -l # 静态库 -rw-r--r-- 1 root root 2932366 Sep 30 2020 /usr/lib/gcc/x86_64-redhat-linux/4.8.2/libstdc++.a1.3 编译器链接行为
一个可执行程序可能用到许多库,这些库运行有的是静态库,有的是动态库。编译器默认使用动态链接库,只有在该目录下找不到动态库.so的时候才会采用同名静态库。我们也可以使用 gcc 的-static选项强制设置链接静态库。
1.4 预备工作:自定义库源码
后续动静态库制作将基于以下自定义源码(模拟文件 IO 和字符串工具库):
(1)文件 IO 库(my_stdio.h/my_stdio.c)
// my_stdio.h #pragma once #include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <string.h> #define MAX 1024 #define NONE_FLUSH (1<<0)//不刷新 #define LINE_FLUSH (1<<1)//行刷新 #define FULL_FLUSH (1<<2)//全刷新 typedef struct IO_FILE { int fileno; //文件描述符 int flag; //文件标志位 char outbuffer[MAX]; //缓冲区 int bufferlen; //当前缓冲区数据长度 int flush_method; //刷新方式(宏) int _capacity; //缓冲区容量 }MYFILE; MYFILE *MyFopen(const char *path, const char *mode); int MyFwrite(const char *str, size_t size, size_t num, MYFILE* fp); void MyFclose(MYFILE *file); void MyFFlush(MYFILE *file);// my_stdio.c #include "mystdio.h" MYFILE *CreatFile(int fd, int flag) { MYFILE *newfile = (MYFILE*)malloc(sizeof(MYFILE)); newfile->bufferlen = 0; newfile->fileno = fd; newfile->flag = flag; newfile->flush_method = LINE_FLUSH; newfile->_capacity = MAX; memset(newfile->outbuffer, 0, sizeof(newfile->outbuffer)); return newfile; } MYFILE *MyFopen(const char *path, const char *mode) { int fd = -1; int flag = 0; if(strcmp(mode, "w") == 0) { //写的方式 flag = O_CREAT | O_WRONLY | O_TRUNC; fd = open(path, flag, 0666); } else if(strcmp(mode, "r") == 0) { //读的方式 flag = O_RDONLY; fd = open(path, flag); } else if(strcmp(mode, "a") == 0) { //追加的方式 flag = O_CREAT | O_WRONLY | O_APPEND; fd = open(path, flag, 0666); } if(fd < 0) return NULL; return CreatFile(fd, flag); } int MyFwrite(const char *str, size_t size, size_t num, MYFILE* file) { //1、拷贝(先判断写入的数据是否会使缓冲区满,如果满了则先进行刷新再写入缓冲区) int sizeNum = size * num; if(file->bufferlen + sizeNum >= file->_capacity) { MyFFlush(file); } memcpy(file->outbuffer + file->bufferlen, str, sizeNum); file->bufferlen += sizeNum; //2、尝试判断是否满足刷新条件 if(file->flush_method == LINE_FLUSH && file->outbuffer[file->bufferlen - 1] == '\n') { MyFFlush(file); } return 0; } void MyFclose(MYFILE *file) { if(file->fileno < 0) return; MyFFlush(file); //系统调用close close(file->fileno); free(file); file = NULL; } void MyFFlush(MYFILE *file) { if(file->bufferlen <= 0) return; //当前缓冲区无数据则无需刷新 //系统调用write直接刷新 // 所谓的刷新就是把数据从用户缓冲区拷贝到内核 // 从用户缓冲区拷贝到内核这种模式叫做WB模式 // WB: Write Back(写回) int n = write(file->fileno, file->outbuffer, file->bufferlen); (void)n; // 刷新到外设,不仅仅要写入到内核缓冲区,还必须写到对应的硬件上 // WT模式,Write Though fsync(file->fileno); //缓冲区刷新完成后缓冲区就没有数据了 file->bufferlen = 0; }(2)字符串库(my_string.h/my_string.c)
// my_string.h #pragma once int my_strlen(const char* s);// my_string.c #include "my_string.h" int my_strlen(const char* s) { const char* end = s; while (*end != '\0') end++; return end - s; }s; }- 总结与引入
二、静态库(编译时链接,独立运行)
静态库(.a)的核心特征是 “编译链接时,将库代码完整拷贝到可执行程序中”,生成的可执行程序不依赖外部库,可独立运行。
2.1 整体图示
- 我们可以先看看这个图示的流程再来往下详细学习
2.2 静态库制作流程(Makefile自动化 ,更简便)
静态库通过ar(GNU 归档工具)制作,核心步骤:编译源码生成.o 文件 → 归档.o 文件为.a 静态库。
2.2.1编写 Makefile
target=libmyc.a src=$(wildcard *.c) obj=$(src:.c=.o) $(target):$(obj) @ar -rc $@ $^ @echo "build $^ to $@ ... done" %.o:%.c @gcc -c $< @echo "compling $< to $@ ... done" .PHONY:output output: @mkdir -p lib/include @mkdir -p lib/mylib @cp -f *.h lib/include @cp -f *.a lib/mylib @tar -czf lib.tgz lib @echo "output lib ... done" .PHONY:clean clean: @rm -rf lib lib.tgz $(target) *.o @echo "clean ... done"ar 是 GNU 归档工具,rc表示replace and create。
- 后续操作如下图所示:
2.2.2 查看静态库内容
$ ar -tv libmystdio.a rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.ot:列出静态库中的文件v:显示详细信息
2.3 静态库使用场景与命令
静态库使用需指定 “头文件路径、库文件路径、库名”,核心命令格式(上面的使用过程中也体现了):
gcc 源文件.c -I头文件路径 -L库文件路径 -l库名 [-static]-I:指定头文件搜索路径(默认搜索/usr/include等系统目录);-L:指定库文件搜索路径(默认搜索/lib等系统目录);-l:指定库名(需去掉前缀lib和后缀.a,如libmyc.a → -l myc);-static:强制链接静态库(优先使用静态库,无静态库则报错)。
场景 1:头文件 / 库文件与源文件同目录
# 编译(同目录下可省略-I) gcc main.c -lmystdio -L . -static场景 2:头文件 / 库文件在独立路径
# 假设库文件在 ./stdc/lib,头文件在 ./stdc/include gcc main.c -I./stdc/include -L./stdc/lib -lmystdio -static场景 3:安装到系统目录(全局可用)
# 拷贝头文件到系统目录 sudo cp *.h /usr/include/ # 拷贝静态库到系统目录 sudo cp libmystdio.a /usr/lib/ # 直接编译(无需指定-I和-L,但是 -l 一定还是必须的) gcc main.c -lmystdio -static2.4 静态库核心特点
- 优点:可执行程序独立运行,不依赖外部库;运行时无需加载库,启动速度快;
测试目标文件生成后,静态库删掉,程序照样可以运行。这是因为静态库的代码已经被链接到可执行文件中。
- 缺点:可执行程序体积大(包含库代码);库更新后需重新编译链接;多个程序使用会重复占用磁盘和内存。
三. 动态库(运行时链接,共享复用)
动态库(.so)是指程序在运行的时候才去链接动态库的代码。一个与动态库链接的可执行文件仅仅包含它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。在可执行文件开始运行以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中,这个过程称为动态链接。
动态库可以在多个程序间共享,所以动态链接使得可执行文件更小,节省了磁盘空间。操作系统采用虚拟内存机制允许物理内存中的一份动态库被要用到该库的所有进程共用,节省了内存和磁盘空间。
3.1 动态库制作流程(Makefile自动化)
3.1.1 编写 Makefile
target=libmyc.so src=$(wildcard *.c) obj=$(src:.c=.o) $(target):$(obj) @gcc -shared -o $@ $^ @echo "build $^ to $@ ... done" # 编译PIC目标文件(位置无关码,支持任意地址加载) %.o:%.c @gcc -fPIC -c $< @echo "compling $< to $@ ... done" .PHONY:output output: @mkdir -p lib/include @mkdir -p lib/mylib @cp -f *.h lib/include @cp -f *.so lib/mylib @tar -czf lib.tgz lib @echo "output lib ... done" .PHONY:clean clean: @rm -rf lib lib.tgz $(target) *.o @echo "clean ... done"关键编译选项:
- -shared:表示生成共享库格式
- -fPIC:产生位置无关码(position independent code)
后续操作如下图所示:
3.2 动态库使用:编译与运行时依赖
动态库编译命令与静态库类似,但运行时需确保系统能找到动态库(否则报错 “libmystdio.so not found”)。
- 步骤 1:编译(同静态库命令,无需 - static)
# 场景1:同目录编译 gcc main.c -L. -lmystdio # 场景2:独立路径编译 gcc main.c -I./stdc/include -L./stdc/lib -lmystdio # 场景3:头文件和库文件安装到系统路径下编译 gcc main.c -lmystdio- 步骤 2:解决运行时库搜索路径
3.2.1 问题现象
$ ldd a.out linux-vdso.so.1 => (0x00007fff4d396000) libmystdio.so => not found libc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000) /lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)3.2.2 解决方案
方法1:拷贝
.so文件到系统共享库路径下
一般指/usr/lib、/usr/local/lib、/lib64或者其他库路径等。
方法2:向系统共享库路径下建立同名软连接
方法3:更改环境变量LD_LIBRARY_PATH
但是我们在前面学习环境变量的时候就讲解了:export不管是修改环境变量还是新增环境变量都只是在当前bash进程中进行操作,也就是说 export 修改变量 → 只改这个进程内部的环境变量表,当我们关闭当前的xshell后,这个bash进程也就结束了。
那么当我们再打开新的xshell时就会创建新的bash进程,也就会生成新的环境变量表,则我们修改的操作也就没了。
那怎么让我们修改的环境变量真正永久保存呢?就需要写到配置文件当中。
方法4:ldconfig 方案
- 配置/etc/ld.so.conf.d/
- ldconfig更新
小结:
3.3 动态库核心特点
- 优点:可执行程序体积小;库更新后无需重新编译(替换.so 文件即可);多个程序共享库代码,节省资源;
- 缺点:运行时依赖动态库,缺失会导致程序无法启动;启动时需加载库,速度略慢于静态库。
四. 动静态库对比与选型建议
| 对比维度 | 静态库 (.a) | 动态库 (.so) |
|---|---|---|
| 链接时机 | 编译、链接阶段 | 程序运行阶段 |
| 可执行程序体积 | 大(库代码被复制进去) | 小(仅记录依赖信息) |
| 运行依赖 | 无需外部文件,独立运行 | 必须依赖对应的.so 文件 |
| 库更新 | 需重新编译、链接整个程序 | 直接替换.so 文件即可生效 |
| 内存占用 | 多个程序重复占用内存 | 多个程序共享内存中的同一份代码 |
| 编译速度 | 快(链接后无需额外处理) | 略慢(需处理动态链接信息) |
| 适用场景 | 小程序、嵌入式(追求无依赖) | 大型项目、多程序共享(节省资源) |
选型建议:
- 若程序需独立部署(如嵌入式设备),选静态库;
- 若追求启动速度和稳定性,选静态库。
- 若多个程序共用同一功能(如公司内部工具库),选动态库;
- 若库更新频繁(如业务逻辑迭代快),选动态库;
相关问题:
五、实战:使用外部库(ncurses 图形库)
除了自定义库,Linux 系统提供大量现成外部库,以ncurses(终端图形库)为例,演示外部库的安装与使用。
5.1 安装 ncurses 库
# CentOS sudo yum install -y ncurses-devel # Ubuntu sudo apt install -y libncurses-dev5.2. 测试代码(大家可以自己试试别的)
#include <ncurses.h> #include <stdlib.h> #include <time.h> #include <unistd.h> #include <locale.h> // 开启UTF-8 #define SPEED 120000 #define MAX_LEN 100 typedef struct { int x; int y; } Node; Node snake[MAX_LEN]; int len = 3; int dir = KEY_RIGHT; int foodX, foodY; int score = 0; void createFood() { foodX = rand() % (COLS - 2) + 1; foodY = rand() % (LINES - 4) + 2; } void initSnake() { len = 3; dir = KEY_RIGHT; score = 0; snake[0].x = COLS / 2; snake[0].y = LINES / 2; snake[1].x = COLS / 2 - 1; snake[1].y = LINES / 2; snake[2].x = COLS / 2 - 2; snake[2].y = LINES / 2; createFood(); } void draw() { clear(); mvprintw(0, 0, "Snake Game | Score: %d | Arrow keys to move | q:Quit", score); mvaddch(foodY, foodX, '*'); for (int i = 0; i < len; i++) { if (i == 0) mvaddch(snake[i].y, snake[i].x, 'O'); // 蛇头换成O else mvaddch(snake[i].y, snake[i].x, '#'); // 蛇身# } refresh(); } void move_snake() { for (int i = len - 1; i > 0; i--) { snake[i] = snake[i - 1]; } switch (dir) { case KEY_UP: snake[0].y--; break; case KEY_DOWN: snake[0].y++; break; case KEY_LEFT: snake[0].x--; break; case KEY_RIGHT: snake[0].x++; break; } } int checkHit() { if (snake[0].x < 0 || snake[0].x >= COLS || snake[0].y < 2 || snake[0].y >= LINES - 1) return 1; for (int i = 1; i < len; i++) { if (snake[0].x == snake[i].x && snake[0].y == snake[i].y) return 1; } return 0; } void eatFood() { if (snake[0].x == foodX && snake[0].y == foodY) { score += 10; len++; createFood(); } } int main() { setlocale(LC_ALL, "en_US.UTF-8"); // 强制UTF-8 initscr(); cbreak(); noecho(); keypad(stdscr, TRUE); curs_set(0); nodelay(stdscr, TRUE); srand((unsigned)time(NULL)); initSnake(); while (1) { draw(); usleep(SPEED); int key = getch(); if (key != ERR) { if (key == KEY_UP && dir != KEY_DOWN) dir = KEY_UP; else if (key == KEY_DOWN && dir != KEY_UP) dir = KEY_DOWN; else if (key == KEY_LEFT && dir != KEY_RIGHT) dir = KEY_LEFT; else if (key == KEY_RIGHT && dir != KEY_LEFT) dir = KEY_RIGHT; else if (key == 'q') break; } move_snake(); if (checkHit()) break; eatFood(); } mvprintw(LINES / 2, COLS / 2 - 6, "Game Over!"); refresh(); sleep(1); endwin(); return 0; }# 编译(-lncurses指定链接ncurses库) gcc test.c -o test -std=c99 -lncurses # 运行 ./test结束语
动静态库是 Linux 开发中实现代码复用的核心机制。熟练掌握其制作、使用方法与选型策略,能够有效提升开发效率、优化项目结构。静态库适用于追求程序独立、简化部署的场景;动态库则更适合多进程共享、节省资源、便于库文件独立更新的场景。
在实际开发中,可根据业务需求灵活选择。本文从基础概念到实战应用,完整覆盖了库开发的核心流程。后续可进一步深入学习库的版本管理、符号隐藏、动态加载(dlopen/dlsym)等高级特性,从而构建更健壮、更灵活的程序体系。
