
目录
[一、静态库 .a](#一、静态库 .a)
[1.1 静态库生成步骤](#1.1 静态库生成步骤)
[Makefile 完整脚本](#Makefile 完整脚本)
[1.2 静态库的三种使用场景](#1.2 静态库的三种使用场景)
[1.3 静态库优缺点](#1.3 静态库优缺点)
[二、动态库 .so(共享库)](#二、动态库 .so(共享库))
[2.1 动态库制作](#2.1 动态库制作)
[2.2 动态库链接使用](#2.2 动态库链接使用)
[2.3 动态库运行时查找的 4 种解决方案](#2.3 动态库运行时查找的 4 种解决方案)
[方案 1:拷贝动态库到系统库目录](#方案 1:拷贝动态库到系统库目录)
[方案 2:软链接](#方案 2:软链接)
[方案 3:环境变量 LD_LIBRARY_PATH(开发最常用)](#方案 3:环境变量 LD_LIBRARY_PATH(开发最常用))
[方案 4:配置 /etc/ld.so.conf.d/ + ldconfig(永久生效)](#方案 4:配置 /etc/ld.so.conf.d/ + ldconfig(永久生效))
[2.4 动态库优缺点](#2.4 动态库优缺点)
[补充小拓展:ncurses 第三方库演示](#补充小拓展:ncurses 第三方库演示)
前言
本节学习重点
- 什么是库,库的分类
- 静态库.a 制作、Makefile 编写、使用方式
- 动态库.so 制作、Makefile 编写、使用方式
- 动态库查找的 4 种解决方案
- 实操演示,常见报错排查
库是什么:库是写好的、成熟的、可以复用的二进制代码。软件开发不可能所有代码从零开始,大量底层功能都封装在库中。本质是可执行二进制,操作系统可以加载进内存运行。
Linux 与 Windows 库文件对比:
表格
| 系统 | 静态库后缀 | 动态库后缀 |
|---|---|---|
| Linux | .a |
.so |
| Windows | .lib |
.dll |
系统自带库示例(CentOS/Ubuntu)
cpp
# CentOS C标准库
ls /lib64/libc.a
ls /lib64/libc-2.17.so
# Ubuntu C标准库
ls -l /lib/x86_64-linux-gnu/libc.a
ls -l /lib/x86_64-linux-gnu/libc-2.31.so
我们自己模拟简易 C 库,准备下面 4 个源文件,作为制作库的素材。
my_stdio.h
cpp
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
struct IO_FILE
{
int flag;
int fileno;
char outbuffer[SIZE];
int cap;
int size;
};
typedef struct IO_FILE mFILE;
mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
my_stdio.c
cpp
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
mFILE *mfopen(const char *filename, const char *mode)
{
int fd = -1;
if(strcmp(mode, "r") == 0)
{
fd = open(filename, O_RDONLY);
}
else if(strcmp(mode, "w")== 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
}
else if(strcmp(mode, "a") == 0)
{
fd = open(filename, O_CREAT|O_WRONLY|O_APPEND, 0666);
}
if(fd < 0) return NULL;
mFILE *mf = (mFILE*)malloc(sizeof(mFILE));
if(!mf)
{
close(fd);
return NULL;
}
mf->fileno = fd;
mf->flag = FLUSH_LINE;
mf->size = 0;
mf->cap = SIZE;
return mf;
}
void mfflush(mFILE *stream)
{
if(stream->size > 0)
{
write(stream->fileno, stream->outbuffer, stream->size);
fsync(stream->fileno);
stream->size = 0;
}
}
int mfwrite(const void *ptr, int num, mFILE *stream)
{
memcpy(stream->outbuffer+stream->size, ptr, num);
stream->size += num;
if(stream->flag == FLUSH_LINE && stream->size > 0 && stream->outbuffer[stream->size-1]== '\n')
{
mfflush(stream);
}
return num;
}
void mfclose(mFILE *stream)
{
if(stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno);
}
my_string.h
cpp
#pragma once
int my_strlen(const char *s);
my_string.c
cpp
#include "my_string.h"
int my_strlen(const char *s)
{
const char *end = s;
while(*end != '\0')end++;
return end - s;
}
一、静态库 .a
核心定义:静态库在编译链接阶段,把库里面目标文件代码拷贝复制到可执行程序内部;程序运行的时候,不再依赖静态库文件本身。
gcc 默认优先链接动态库;如果同一个目录同时存在同名.so和.a,优先选.so。使用-static强制编译器使用静态链接。
1.1 静态库生成步骤
- 把
.c源文件编译生成目标文件*.o,gcc -c,只编译不链接。 - 使用
ar归档工具,把若干.o打包成静态库libxxx.a。
ar是 GNU 归档工具:
-rreplace:替换 / 新增目标文件进库-ccreate:创建库文件-t:查看库里面包含哪些.o文件-vverbose,显示详细过程
Makefile 完整脚本
cpp
libmystdio.a:my_stdio.o my_string.o
@ar -rc $@ $^
@echo "build $^ to $@ ... done"
%.o:%.c
@gcc -c $<
@echo "compling $< to $@ ... done"
.PHONY:clean
clean:
@rm -rf *.a *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.a stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
执行 make,生成libmystdio.a。 查看库内包含的目标文件:
ar -tv libmystdio.a
注意库命名规范:必须以
lib开头,后缀.a;链接时-lmystdio,自动去掉前缀lib和后缀.a。
1.2 静态库的三种使用场景
测试代码main.c
cpp
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main()
{
const char *s = "abcdefg";
printf("%s: %d\n", s, my_strlen(s));
mFILE *fp = mfopen("./log.txt", "a");
if(fp == NULL) return 1;
mfwrite(s, my_strlen(s), fp);
mfwrite(s, my_strlen(s), fp);
mfwrite(s, my_strlen(s), fp);
mfclose(fp);
return 0;
}
表格
| 场景 | 命令 | 说明 |
|---|---|---|
| 头文件、库安装到系统路径 | gcc main.c -lmystdio |
系统自动搜索系统头文件、系统库路径 |
| 头文件、库在当前目录 | gcc main.c -L. -lmystdio |
-L. 指定库搜索路径为当前目录 |
| 头文件、库独立自定义路径 | gcc main.c -I./include -L./lib -lmystdio |
-I 指定头文件搜索路径;-L指定库搜索路径 |
参数说明
-I:指定头文件搜索路径-L:指定库文件搜索路径-lxxx:链接名字为libxxx的库
✅静态库特性验证:链接完成后,把 libmystdio.a 删掉,程序依旧可以正常运行,代码已经拷贝进可执行程序。
1.3 静态库优缺点
✅优点
- 运行时不依赖库文件,程序移植简单,拷贝 exe 直接运行;
- 运行时不需要动态链接,启动速度快。
❌缺点
- 可执行文件体积大;
- 多个程序使用同一个静态库,内存会有多份库代码副本,内存浪费;
- 如果库升级,所有程序必须重新编译链接,不能直接替换库文件。
二、动态库 .so(共享库)
核心定义:动态库不会把代码拷贝进可执行程序;链接阶段只记录依赖信息,等到程序运行时,操作系统才把动态库加载映射到进程虚拟地址空间;物理内存中一份动态库可以被多个进程共享。
可执行文件中,只保存函数的入口引用表,没有库函数完整机器码。运行时由操作系统把磁盘上.so加载到内存,这个过程叫动态链接。
2.1 动态库制作
关键编译选项:
-fPIC:生成位置无关代码(Position Independent Code),动态库必须。代码全部使用相对寻址,可以加载到虚拟地址空间任意位置。-shared:生成共享库(动态库)。
Makefile 脚本
cpp
libmystdio.so:my_stdio.o my_string.o
gcc -o $@ $^ -shared
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
@rm -rf *.so *.o stdc*
@echo "clean ... done"
.PHONY:output
output:
@mkdir -p stdc/include
@mkdir -p stdc/lib
@cp -f *.h stdc/include
@cp -f *.so stdc/lib
@tar -czf stdc.tgz stdc
@echo "output stdc ... done"
注意:生成
.o目标文件必须加-fPIC,不能省略!
2.2 动态库链接使用
同样使用上面的main.c测试代码,链接命令和静态库几乎一模一样:
cpp
# 当前目录下有libmystdio.so
gcc main.c -L. -lmystdio -o app
链接成功生成app可执行程序,但是直接执行 ./app 会报错 :error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory。
🔔区分两个阶段路径:
- 编译链接阶段 :
-L告诉编译器去哪里找库,编译时生效;- 程序运行阶段 :操作系统动态链接器
ld-linux.so会去系统预设路径找.so,此时-L参数已经失效。
使用ldd命令,查看可执行程序依赖哪些动态库:
ldd app
输出示例,可以看到我们自己写的库显示not found。
cpp
linux-vdso.so.1 => (0x00007fffacbbf000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007f8917335000)
/lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)
2.3 动态库运行时查找的 4 种解决方案
方案 1:拷贝动态库到系统库目录
系统库目录:/usr/lib、/usr/local/lib、/lib64
cpp
sudo cp libmystdio.so /usr/lib
./app
缺点:污染系统库,不推荐开发环境频繁使用。
方案 2:软链接
把我们库所在路径创建软链接到系统库目录。
cpp
sudo ln -s /home/xxx/code/libmystdio.so /usr/lib/libmystdio.so
方案 3:环境变量 LD_LIBRARY_PATH(开发最常用)
把动态库所在目录添加到环境变量LD_LIBRARY_PATH,动态链接器优先读取这个环境变量。
cpp
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
./app
注意:仅当前终端会话生效;新开终端环境变量失效。适合开发调试。
方案 4:配置 /etc/ld.so.conf.d/ + ldconfig(永久生效)
- 在
/etc/ld.so.conf.d/目录新建配置文件,例如mybit.conf,写入动态库所在绝对路径。
cpp
sudo vim /etc/ld.so.conf.d/mybit.conf
# 文件内写入:/home/xxx/code
-
执行
ldconfig,更新系统动态库缓存/etc/ld.so.cachesudo ldconfig
原理:动态链接器优先读取缓存文件
ld.so.cache,缓存保存所有可用动态库路径。修改配置文件后必须执行 ldconfig 刷新缓存,否则不生效。
2.4 动态库优缺点
✅优点
- 磁盘节省空间,多个程序共用同一份
.so文件; - 物理内存只加载一份库,多进程共享,节约内存;
- 库升级只替换
.so文件,不需要重新编译应用程序。
❌缺点
- 运行依赖
.so库文件,部署必须同步拷贝库,否则程序直接无法运行; - 程序启动需要动态链接做符号解析,启动会有少量开销。
补充小拓展:ncurses 第三方库演示
系统存在大量现成库,举例 ncurses 字符图形库。 安装:
cpp
# CentOS
sudo yum install -y ncurses-devel
# Ubuntu
sudo apt install -y libncurses-dev
测试 demo
cpp
#include <stdio.h>
#include <string.h>
#include <ncurses.h>
#include <unistd.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
int main() {
initscr();
start_color();
init_pair(1, COLOR_GREEN, COLOR_BLACK);
init_pair(2, COLOR_RED, COLOR_BLACK);
cbreak();
noecho();
curs_set(FALSE);
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);
box(win, 0, 0);
wrefresh(win);
int progress = 0;
int max_progress = PROGRESS_BAR_WIDTH;
while (progress <= max_progress) {
werase(win);
int completed = progress;
int remaining = max_progress - progress;
int bar_x = BORDER_PADDING + 1;
int bar_y = 1;
attron(COLOR_PAIR(1));
for (int i = 0; i < completed; i++) {
mvwprintw(win, bar_y, bar_x + i, "#");
}
attroff(COLOR_PAIR(1));
attron(A_BOLD | COLOR_PAIR(2));
for (int i = completed; i < max_progress; i++) {
mvwprintw(win, bar_y, bar_x + i, " ");
}
attroff(A_BOLD | COLOR_PAIR(2));
char percent_str[10];
snprintf(percent_str, sizeof(percent_str), "%d%%", (progress * 100) / max_progress);
int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2;
mvwprintw(win, WINDOW_HEIGHT - 1, percent_x, percent_str);
wrefresh(win);
progress += PROGRESS_INCREMENT;
usleep(DELAY);
}
delwin(win);
endwin();
return 0;
}
编译链接:gcc main.c -lncurses -o bar
上篇小结
- 静态库
.a:编译链接期拷贝目标文件代码到可执行程序;运行不需要库文件;ar -rc打包; - 动态库
.so:运行时加载,多进程共享内存;编译加-fPIC -shared; - 动态库运行找不到库四种解决方案:拷贝系统目录、软链接、
LD_LIBRARY_PATH环境变量、/etc/ld.so.conf.d+ldconfig; - 编译阶段找库看
-L;运行阶段找库看动态链接器ld-linux.so。
