本篇目标:
掌握动态库的基本原理及实际应用示例
一.动态库的原理与实战
1.代码准备
文件一:接口定义 my_lib.h
cpp
#ifndef MY_LIB_H
#define MY_LIB_H
// 简单的加法,用来演示函数导出
int add(int a, int b);
// 打印库的信息,证明我们在调用动态库
void print_lib_info();
#endif
文件二:库的实现 my_lib.c
cpp
#include <stdio.h>
#include "my_lib.h"
int add(int a, int b)
{
return a + b;
}
void print_lib_info()
{
printf("[Dynamic Library] 这是一个由 C 语言生成的动态库 (.so)\n");
printf("[Dynamic Library] 它的代码段在内存中是共享的。\n");
}
文件三:调用程序 main.c
cpp
#include <stdio.h>
#include "my_lib.h"
int main()
{
printf("--- 主程序启动 ---\n");
print_lib_info();
int result = add(10, 20);
printf("10 + 20 = %d\n", result);
printf("--- 主程序结束 ---\n");
return 0;
}
目前这是在同一个目录下,生成的,如图所示:
为了模拟系统中的动态库,我们还要和之前的静态库一样,形成如图所示的目录:

也就是**./shared_code/lib** 和**./shared_code/usr/include**
2.动态库的形成
先复习一下之前的选项:
-c: 把汇编代码翻译成纯粹的机器码,生成目标文件(.o 文件),它只编译,绝对不进行链接(不去找动态库或静态库)。
-o:作用: 让gcc生成文件的指定的名字。
由于所有的库都是由.o文件组成的,因此我们需要先将想要形成库的.c文件转化为.o文件,但是还
需要一个选项,也就是**-fPIC,作用是生成位置无关代码,让库加载到内存任意地址都能正常运行**
操作如图所示:

然后再将其打上共享标签 ,此时要用**-shared** 选项,封装成 .so 文件 ,操作如图所示:

-shared:告诉链接器生成一个用来被别人动态加载的共享对象(了解即可)
注意:当生成多个.o库文件时,可以将它们统一打包成一个共享库,此时我们可以使用
gcc -fPIC -c *.c 和gcc -shared -o libmyc.so *.o,但是绝对不能把 main 函数的.o文件打包进动态
库里!
这里再提醒一下关于动态库的命名: lib + 库的真实名字 + .so
然后我们在将当前目录下的动态库与库的头文件,拷贝到我们自己创建的目录下,如图所示:

注意:建议使用 cp 命令进行复制操作,而非 mv 命令。若误用 mv 命令,当前目录下的文件会被移
动而非复制(之前犯过的错误)。

在上级目录中,我们查看一下shared_code是否成功有拷贝后的文件,如图所示:

此时我们需要删除code目录下的libmyc.so文件以及除main.c之外的其他文件 ,以免影响后续操作,如图所示:
然后我们在将main.c编译成main.o,如图所示:

可是我们却发现找不到这个my_lib.h文件啊?
原因:当你使用 #include "my_header.h" 时,gcc 默认只会在以下几个地方寻找头文件:
-
当前目录 (也就是**
main.c所在的目录**) -
系统默认的头文件目录 (比如 **
/usr/include,/usr/local/include**等)。
但是gcc根本不知道 ../shared_code/usr/include 这个神奇的目录存在,所以肯定会编译中断。
因此我们需要这样操作:

那么此时就会有人说:这也不对啊,我以前使用gcc时,不也用到了库吗,但是我也没有用-I来指
明我要查找的头文件啊?
解释:gcc认识/usr/include与usr/local/include,就会自动在这里面找头文件,但是gcc并不认识
我们自己的my_lib.h。
当我们生成了可执行程序,需要调用这个动态库时,操作逻辑与链接静态库如出一辙。我们需要向编译器(gcc)准确传达三个维度的寻址信息:
-
-I:指引头文件的搜索目录。就像是告诉编译器:去这个指定的文件柜里,翻阅函数的接口说明书。 -
-L:指引库文件的搜索目录。就像是告诉链接器:别光在系统默认仓库找,去我指定的这个额外仓库里找找高阶零件。 -
-l:指定需要链接的具体库名 。它紧跟在-L之后,明确点出零件的代号(例如-lmyc会自动去寻找libmyc.so)。
操作如图所示:

最后来到了最终时刻,执行这个可执行程序,如图所示:

我们发现可执行程序main无法运行,原因是加载时找不到所需的库文件。虽然我们已通过-l参数
gcc在../shared_code/lib目录下查找,但问题依然存在。于是我们使用ldd命令来验证库文件是否确
实未被找到,结果如图所示:
解释:虽然gcc能够识别libmyc.so,但系统本身并不认识该库文件。生成可执行程序后,程序运
行将由系统接管。系统会在预设的库文件路径中查找依赖库,而我们的../shared_code/olib目录并
不在这些系统默认搜索路径中,因此会导致库文件找不到的问题。
解决办法:
1.系统找不到,那我们就在运行前,临时强行塞给它一个搜索路径,我们让系统在
LD_LIBRARY_PATH 环境变量中查找库,操作如图所示:

但是这个是临时指定搜索路径,关闭终端后就失效了。
2.干脆在编译的时候,就把运行期的寻找路径死死烙印在可执行文件的骨子里。
操作:
cpp
gcc -o main main.o -I ../shared_code/usr/include -L ../shared_code/lib -lmyc -Wl,-rpath=../shared_code/lib
如图所示:

也是成功了的。
3.系统只认 /usr/lib 或者 /lib 这种目录,那我们直接把自己的库扔进这里面就行了,但是这样
会污染系统目录(不太建议这样做)。
4.修改系统的动态加载器配置文件,把你的目录合法注册进去。
操作:首先获取库的绝对路径 (假设是 /home/kong/shared_code/lib);其次,在
/etc/ld.so.conf.d/ 目录,用sudo touch新建一个文件 (比如 myc.conf),用sudo vim打开
这个文件 ,把绝对路径写进去;最后,刷新系统缓存,sudo ldconfig。
如图所示:


此时在执行,就成功了。
3.补充点
1.GCC/G++ 默认采用动态链接方式,如需使用静态库,需添加 -static 参数,且系统中必须存在对应的静态库文件。
我们先创建一个.c文件,代码如图所示:
cpp
#include<stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
操作如图所示:
通过ldd后的libc.so,也就是c语言的动态库,我们可以看到code默认是动态链接的,此时code的大
小为8360字节,也验证了我们的猜想,如果是静态链接呢?首先我们需要先安装静态库,在
centos下的安装操作:
cpp
sudo yum install glibc-static
操作如图所示:
通过code的大小与ldd的信息,可以确定这是个静态链接。
2.Linux 系统中,大多数库在安装时默认优先安装动态库版本。
直接操作,如图所示:
cpp
# 统计动态库 (.so) 的数量
find /usr/lib /usr/lib64 -name "*.so*" 2>/dev/null | wc -l
cpp
# 统计静态库 (.a) 的数量
find /usr/lib /usr/lib64 -name "*.a" 2>/dev/null | wc -l
注:目前这个操作不需要认识,直接复制粘贴
结果如图所示:

可以看到动态库的数量是大几千,而静态库的数量少得可怜!这就直接从系统生态的层面上证明了:Linux 官方和各大软件厂商,默认发放和安装的都是动态库。
4.总结
- 动态库制作:必须使用 -fPIC 生成位置无关代码,再用 -shared 打包成 .so 文件。
- 编译链接阶段:需要使用 -I(头文件路径)、-L(库文件路径)、-l(库名)三个参数准确指引 gcc。
- 运行时加载 :gcc 能找到库 ≠ 系统运行时能找到库 。系统动态加载器(ld.so)默认只在标准路径搜索,因此需要通过以下方式解决:
- LD_LIBRARY_PATH:临时设置(测试推荐)
- -Wl,-rpath:编译时嵌入路径(项目推荐)
- ldconfig:永久注册到系统(公共库推荐)
- 动态链接 vs 静态链接:Linux 系统默认优先使用动态库,程序体积更小,内存共享效率更高,这也是现代软件的主流方式。
二.外部库
我们现在没接触过太多的库,唯⼀接触过的就是C、C++标准库,这⾥我们可以推荐⼀个好玩的图形 库:ncurses,安装操作:
cpp
// Centos下
sudo yum install -y ncurses-devel
// ubuntu下
sudo apt install -y libncurses-dev
系统中其实有很多库,它们通常由⼀组互相关联的⽤来完成某项常⻅⼯作的函数构成。⽐如⽤来处理 屏幕显⽰情况的函数(ncurses库)(看看就行)
代码如图所示:
cpp
#include <ncurses.h>
#include <unistd.h>
#include <string.h>
#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2) // 加边框的宽度
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000 // 微秒(300毫秒)
int main() {
// 1. 初始化 ncurses 环境
initscr();
start_color();
cbreak();
noecho();
curs_set(FALSE); // 隐藏光标,让界面更干净
// 2. 初始化颜色对
init_pair(1, COLOR_GREEN, COLOR_BLACK); // 已完成部分
init_pair(2, COLOR_RED, COLOR_BLACK); // 剩余部分
// 3. 计算屏幕居中坐标并创建窗口
int max_y, max_x;
getmaxyx(stdscr, max_y, max_x);
int start_y = (max_y - WINDOW_HEIGHT) / 2;
int start_x = (max_x - WINDOW_WIDTH) / 2;
WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);
int progress = 0;
int max_progress = PROGRESS_BAR_WIDTH;
// 4. 核心渲染循环
while (progress <= max_progress) {
werase(win); // 清除上一帧内容
box(win, 0, 0); // 必须在擦除后立刻重新绘制边框!
// 防溢出保护,确保进度不超过最大值
int completed = (progress > max_progress) ? max_progress : progress;
int bar_x = BORDER_PADDING + 1;
int bar_y = 1; // 进度条放在第 2 行 (y=1)
// 渲染已完成部分 (注意必须用 wattron)
wattron(win, COLOR_PAIR(1));
for (int i = 0; i < completed; i++) {
mvwprintw(win, bar_y, bar_x + i, "#");
}
wattroff(win, COLOR_PAIR(1));
// 渲染剩余部分
wattron(win, A_BOLD | COLOR_PAIR(2));
for (int i = completed; i < max_progress; i++) {
mvwprintw(win, bar_y, bar_x + i, " ");
}
wattroff(win, A_BOLD | COLOR_PAIR(2));
// 渲染百分比文本
char percent_str[10];
int percent_val = (completed * 100) / max_progress;
snprintf(percent_str, sizeof(percent_str), "%d%%", percent_val);
int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2;
// 打印在进度条下方,避开底部边框 (y=3)
mvwprintw(win, 3, percent_x, percent_str);
wrefresh(win); // 刷新窗口,将缓冲区推送到屏幕
progress += PROGRESS_INCREMENT;
usleep(DELAY);
}
// 跑满 100% 后停顿 1 秒,让你能看清结果再退出
sleep(1);
// 5. 优雅清理资源
delwin(win);
endwin();
return 0;
}
我们此时用到了第三方动态库 ncurses,虽然 库中有这个**ncurses**库,但是gcc不认识它,就要用-l
和这个库指定连接**,也就是让**让 GCC 去系统的公共大仓库里,精准提取这一个特定的库文件。
cpp
gcc main.c -o main -lncurses
结果如图所示: