库 = 预先写好、可直接复用的二进制代码。我们写的 C/C++ 程序,几乎都离不开系统库(libc、libstdc++),自己封装工具代码做成库,能实现:
- 代码复用,不用重复造轮子
- 二进制发布,不暴露源码
- 模块化开发,大型项目必备
一、什么是库
库是写好的 , 现有的 、 成熟的 、 可以复用的代码的二进制形式 。 每个程序都要依赖很多底层库 , 比如C标准库 libc , C++标准库 libstdc++ 。 库的存在极大地提高了开发效率和代码复用性 。
本质上 , 库是一种可执行代码的二进制形式 , 可以被操作系统载入内存执行 。 根据链接时机不同 , 库分为两种:
静态库 :在Linux中后缀为
.a(archive),在Windows中为.lib。程序在编译链接时,会将库中的代码直接拷贝到最终的可执行文件中。因此,程序运行时不再需要静态库。动态库 :在Linux中后缀为
.so(shared object),在Windows中为.dll。程序在运行时才去链接动态库的代码,多个程序可以共享同一个动态库。
核心区分动静态库:
- 静态库 :编译时复制代码到可执行文件 → 运行不需要库
- 动态库 :编译时只记地址 → 运行必须依赖库


可以看到,系统同时提供了静态库(.a)和动态库(.so)。默认情况下,gcc优先使用动态链接,只有当动态库不存在时才会考虑静态库,或者使用 -static 选项强制静态链接。
情景解释: 如果突然某一天,电脑报错 , 出现dll(window下的动态库)缺失 , 可能是由于软件更新 或者 电脑的杀毒软件更新毒源 , 病毒库 ,把dll文件看成病毒, 干掉了

demo:
为了深入理解,我们来实现一个迷你的标准I/O库,包含文件打开、写入、刷新、关闭等功能,以及一个字符串长度函数。这个示例将贯穿全文。
mystdio.h:
#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; // 当前缓冲区中数据大小
// TODO 可扩展
};
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);
mystdio.c:
#include <sys/types.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include "mystdio.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)
{
// 1. 拷贝数据到缓冲区
memcpy(stream->outbuffer + stream->size, ptr, num);
stream->size += num;
// 2. 检测是否要刷新(行缓冲且遇到换行符)
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);
free(stream);
}
mystring.h:
#pragma once
int my_strlen(const char *s);
my_string.c:
#include "mystring.h"
int my_strlen(const char *s)
{
const char *end = s;
while(*end != '\0') end++;
return end - s;
}
usercode.c
#include "mystdio.h"
#include <string.h>
#include <unistd.h>
#include "mystring.h"
#include <stdio.h>
int main()
{
MYFILE * filep = MyFopen("./log.txt","a");
if(!filep)
{
printf("fopen error!\n");
return 1;
}
int cnt = 10;
while(cnt--)
{
char *msg = (char*)"helllo myfile!!!";
MyFwrite(filep,msg,strlen(msg));
MyFflush(filep);
printf("buffer:%s\n",filep->outbuffer);
sleep(1);
}
MyClose(filep);//FILE *fp
const char* str = "hello bit\n";
printf("strlen: %d\n",my_strlen(str));
return 0;
}
以上代码模拟了标准I/O库的缓冲机制和文件操作。我们将以此为基础制作静态库和动态库。
如果现在,我写了很大的一个工程项目 , 里面有很多个 .c 和 .h文件 , 别人想要购买我这个项目 , 但是我希望隐藏源代码 (.c) 文件 , 仅提供给别人 (.o) 文件 和接口说明(.h) ,供他人使用 。引出我们接下来讲的动静态库!
1. 目标文件(.o文件)
通过 gcc -c将.c源文件预处理、编译、汇编生成,包含机器代码但未链接。
2. 头文件(.h文件)
本质是方法声明文档,包含函数原型、结构体定义等,供调用者参考。
3. 静态库(.a文件)
将多个.o文件打包为单个归档文件(.a),便于分发和管理。
二、静态库
- 静态库本质上是一个归档文件,它将多个目标文件(.o)打包成一个文件 ,并建立了索引以便链接器快速查找。
- .a静态库 :**本质是一种归档文件,不需要使用者解包,**而用gcc/g++直接进行链接即可!!!!
- 一个可执行程序可能用到很多库 , 这些库运行有的是静态库 , 有的是动态库,而我们的编译的时候默认是动态库 , 只有在该库下找不到动态库 (以.so )的时候,才会采用同名的静态库!!!我们也可以使用gcc的**-static** 强转设置链接静态库
- 编译链接时,把库中代码直接拷贝进可执行文件 。
- 优点:运行不依赖库,移植方便。
- 缺点:可执行文件大,多个程序复用浪费磁盘 / 内存。
生成静态库的步骤:
编译源文件为目标文件 :gcc -c my_stdio.c mystring.c
使用
ar工具归档 :ar -rc libmyc.a
2.1 静态库生成
Makefile:
libmyc.a: mystudio.o mystring.o
ar -rc $@ $^
mystudio.o: mystudio.c
gcc -c $<
mystring.o: mystring.c
gcc -c $<
.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
.PHONY: clean
clean:
rm -f *.o libmyc.a lib.tgz
rm -rf lib


逐行解释Makefile:
libmyc.a: mystudio.o mystring.o:目标libmyc.a依赖于两个 .o 文件。
ar -rc $@ $^:ar是归档工具,-r表示替换或插入文件 ,-c表示创建归档 。$@是目标文件名(libmyc.a),$^是所有依赖文件(mystudio.o mystring.o)。@前缀表示不显示命令本身。
%.o: %.c:模式规则,任何 .o 文件都由对应的 .c 文件通过gcc -c生成。
clean伪目标:删除所有生成的文件。
output伪目标:创建目录结构,将头文件和静态库拷贝到lib/include和lib/mylib,然后打包成 lib.tgz,方便分发。执行
make后,会生成libmyc.a。我们可以用ar -tv查看库中包含的目标文件:
2.2 静态库使用



库的名称 :去掉前缀 lib + 后缀 .a (静态库) 或者 .so (动态库), 剩下的就是 库的名称


- 通过静态库 , 我们开发者在给别人可执行项目的时候 ,就可以不给源码,给相对应的.o 文件以及 .h 文件即可!
- 测试目标文件生成后 , 静态库删掉,程序照样可以运行 , ,因为库代码已经被整合到可执行文件中。
- 关于
-static选项:如果我们希望强制使用静态链接(即使系统中有动态库)- 静态链接的可执行文件,体积更大,但不依赖任何外部动态库。


三、动态库
动态库在程序运行时才被加载 ,多个程序可以共享内存中的同一份库代码,节省内存和磁盘空间。

3.1 动态库生成
libmyc.so: mystudio.o mystring.o
gcc -shared -o $@ $^
mystudio.o: mystudio.c
gcc -fPIC -c $<
mystring.o: mystring.c
gcc -fPIC -c $<
.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
.PHONY: clean
clean:
rm -f *.o libmyc.so lib.tgz
rm -rf lib

libmyc.so:将mystudio.o和mystring.o打包为动态共享库,-shared选项指定生成动态库。.o目标 :**编译.c文件时添加-fPIC选项,生成位置无关代码,**这是生成动态库的必要条件。output伪目标 :创建目录结构,拷贝头文件和动态库,最后将整个lib目录打包为lib.tgz。clean伪目标:清理所有编译生成的中间文件、动态库、压缩包和目录。
3.2 动态库使用


3.3 库运行搜索路径
3.3.1 问题

可执行程序都摆在这儿了,为什么又打不开 ??? 凭啥???
ldd 可执行程序 : 查看可执行程序关联了那个库!!!

- 你告诉谁了 ? 你只告诉了gcc !但是系统不知道,不知道去哪里找动态库(运行时错误) ;
- 编译时 :你用
-L ./lib/mylib/告诉了gcc链接器库文件在哪里,所以能成功生成可执行文件。- 运行时 :系统动态链接器(
ld-linux.so)不知道这个路径,它只会去系统默认库路径(如/lib、/usr/lib)和LD_LIBRARY_PATH里找,找不到libmyc.so就会报错。简单说:-L 只管编译链接,不管运行时加载。
- 静态库没有这个问题 , 为什么? 静态库链接的时候,是把程序拷贝到可执行程序里,一旦可执行程序创建成功,就不再依赖静态库了;只要编译成功,就一定能运行
3.3.2 解决方案
方法1:将库文件拷贝到系统库目录(需要root权限)



方法2:在系统库目录下建立软链接

取消软连接

方法3:设置环境变量 LD_LIBRARY_PATH
这种方法只对当前终端有效,适合临时测试。
$ export LD_LIBRARY_PATH=. # 将当前目录加入搜索路径
$ ./a.out

export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:/home/zs/linux/lesson23/zs/lib/mylib/

- 用
export命令设置的环境变量,只在当前终端会话(进程)中生效 ,属于临时生效。- 一旦关闭终端,这个环境变量就会被系统回收,下次打开新终端时,
LD_LIBRARY_PATH会恢复为空或默认值,程序就又找不到动态库了。LD_LIBRARY_PATH是内存级的临时环境变量,仅当前终端进程可见,关闭终端后配置就会丢失。- 想要有效,就需要写到配置文件里
方法4:修改 /etc/ld.so.conf 配置文件
在
/etc/ld.so.conf.d/下新建一个配置文件,如my.conf,内容为库所在目录的绝对路径,然后执行sudo ldconfig。
$ sudo sh -c "echo /absolute/path/to/lib > /etc/ld.so.conf.d/my.conf"
$ sudo ldconfig

3.4 动态库的优点
-
节省磁盘空间:多个程序共享同一份库文件,而不是每个程序都拷贝一份。
-
节省内存:操作系统通过虚拟内存机制,物理内存中只保留一份动态库,被所有使用该库的进程共享。
-
易于更新:只需替换库文件,无需重新链接所有程序(前提是接口保持不变)。
3.5 相关知识补充
【库里面能不能不动态库和静态库都同时存在 】
可以,在linux系统下,默认情况安装的大部分库,都是动态库


- 如果非要使用静态链接 , 使用 --static 选项;
- 如果只存在静态库 ,可执行程序就只能使用静态链接了!!!
- 库 : 应用程序 = 1 : n
- vs 不仅仅可以形成可执行程序 , 也能形成动静态库
四、使用外部库
实际开发中我们经常使用第三方库,比如图形库 ncurses。下面以一个简单的进度条程序为例,展示如何使用外部库。
4.1 安装ncurses开发包
在CentOS上
$ sudo yum install -y ncurses-devel
在Ubuntu上:
$ sudo apt install -y libncurses-dev
4.2 示例程序:进度条
以下代码使用ncurses库在终端显示一个动态进度条
#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 // 微秒 (300毫秒)
int main() {
initscr(); // 初始化ncurses模式
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_y = 1;
int bar_x = BORDER_PADDING + 1;
// 显示已完成部分(绿色)
attron(COLOR_PAIR(1));
for (int i = 0; i < completed; i++) {
mvwprintw(win, bar_y, bar_x + i, "#");
}
attroff(COLOR_PAIR(1));
// 显示剩余部分(红色背景,但只打印空格模拟)
attron(COLOR_PAIR(2));
for (int i = completed; i < max_progress; i++) {
mvwprintw(win, bar_y, bar_x + i, " ");
}
attroff(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 - 2, percent_x, percent_str);
wrefresh(win); // 刷新窗口
progress += PROGRESS_INCREMENT;
usleep(DELAY); // 延迟
}
// 清理并退出ncurses模式
delwin(win);
endwin();
return 0;
}
编译时需要链接ncurses库:
$ gcc progress.c -lncurses -o progress
$ ./progress

使用外部库的一般步骤:
-
安装开发包(通常包含头文件和库文件)。
-
在代码中包含相应头文件。
-
编译时用
-l指定库名(如-lncurses),必要时用-L指定库路径。
五、从源文件到目标文件

当我们执行 gcc -c hello.c 时,会生成一个目标文件 hello.o。目标文件是二进制格式,但还不是可执行文件,它包含了编译后的机器指令和数据,但其中的外部符号地址尚未确定。
查看目标文件的类型:
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
这表明 hello.o 是一个ELF格式的可重定位文件(relocatable file)。
把 .c 编译形成 .o 文件 , 与其他文件没有任何关系 , 只有在链接的时候会和其他文件形成关系;如果存在一个 .c 文件出错 , 修改之后重新生成 .o 文件 , 只会修改同名的 .o 文件,不影响其他的 .o 文件 !!!!
-> 以较小的成本,把代码进行重新编译!!!!