目录
[一、什么是库?------ 二进制的 "代码积木"](#一、什么是库?—— 二进制的 “代码积木”)
[二、静态库 ------"打包带走" 的代码](#二、静态库 ——“打包带走” 的代码)
[2.1 准备源码 ------ 造一个 "迷你标准库"](#2.1 准备源码 —— 造一个 “迷你标准库”)
[2.1.1 头文件:my_stdio.h](#2.1.1 头文件:my_stdio.h)
[2.1.2 源文件:my_stdio.c](#2.1.2 源文件:my_stdio.c)
[2.1.3 字符串处理:my_string.c](#2.1.3 字符串处理:my_string.c)
[2.2 制作静态库 ------ 把代码 "打包" 成.a 文件](#2.2 制作静态库 —— 把代码 “打包” 成.a 文件)
[2.2.1 编写 Makefile](#2.2.1 编写 Makefile)
[2.2.2 关键命令解释](#2.2.2 关键命令解释)
[2.3 使用静态库 ------ 链接成可执行程序](#2.3 使用静态库 —— 链接成可执行程序)
[2.3.1 测试程序:main.c](#2.3.1 测试程序:main.c)
[2.3.2 编译链接 ------ 三种场景](#2.3.2 编译链接 —— 三种场景)
[场景 1:头文件和库文件在系统路径](#场景 1:头文件和库文件在系统路径)
[场景 2:头文件和库文件在当前目录](#场景 2:头文件和库文件在当前目录)
[场景 3:头文件和库文件在自定义路径](#场景 3:头文件和库文件在自定义路径)
[2.3.3 验证静态库的独立性](#2.3.3 验证静态库的独立性)
[2.4 静态库的优缺点 ------ 优点缺点同样明显](#2.4 静态库的优缺点 —— 优点缺点同样明显)
[三、动态库 ------"共享使用" 的代码](#三、动态库 ——“共享使用” 的代码)
[3.1 制作动态库 ------ 生成.so 文件](#3.1 制作动态库 —— 生成.so 文件)
[3.1.1 编写 Makefile](#3.1.1 编写 Makefile)
[3.2 使用动态库 ------ 编译链接 + 运行时加载](#3.2 使用动态库 —— 编译链接 + 运行时加载)
[3.2.1 编译测试程序](#3.2.1 编译测试程序)
[3.2.2 解决动态库找不到的问题 ------ 四种方案](#3.2.2 解决动态库找不到的问题 —— 四种方案)
[方案 1:拷贝动态库到系统路径](#方案 1:拷贝动态库到系统路径)
[方案 2:设置环境变量 LD_LIBRARY_PATH](#方案 2:设置环境变量 LD_LIBRARY_PATH)
[方案 3:添加软链接到系统路径](#方案 3:添加软链接到系统路径)
[方案 4:配置 /etc/ld.so.conf.d](#方案 4:配置 /etc/ld.so.conf.d)
[3.2.3 查看程序依赖的动态库](#3.2.3 查看程序依赖的动态库)
[3.3 动态库的优缺点 ------ 空间换时间的典范](#3.3 动态库的优缺点 —— 空间换时间的典范)
[四、动静态库的核心区别 ------ 底层逻辑大揭秘](#四、动静态库的核心区别 —— 底层逻辑大揭秘)
[4.1 链接过程的区别 ------ 静态链接是 "复制",动态链接是 "记录"](#4.1 链接过程的区别 —— 静态链接是 “复制”,动态链接是 “记录”)
[4.2 内存加载的区别 ------ 静态库是 "私有",动态库是 "共享"](#4.2 内存加载的区别 —— 静态库是 “私有”,动态库是 “共享”)
[4.3 直观对比 ------ 一张表看懂区别](#4.3 直观对比 —— 一张表看懂区别)
[五、拓展:试试 ncurses 图形库](#五、拓展:试试 ncurses 图形库)
前言
在 C/C++ 开发的世界里,库就像是程序员的 "神兵利器"。它把成熟、可复用的代码打包成二进制文件,让我们不用重复造轮子,直接站在巨人的肩膀上开发。你写的每一个
printf、每一次字符串操作,其实都在和库打交道。今天我们就彻底撕开动静态库的神秘面纱,从原理剖析 到实战操作,手把手带你搞懂它们的区别、制作方法和底层逻辑。下面就让我们正式开始吧!
一、什么是库?------ 二进制的 "代码积木"
首先我们要明确:库是可复用代码的二进制形式,能被操作系统载入内存执行。它就像乐高积木,不同的积木块(库函数)可以组合出各种复杂的程序(模型)。
从类型上看,库主要分为两大类:
- 静态库 :Linux 下后缀是
.a,Windows 下是.lib。程序编译链接时,会把库的代码直接复制到可执行文件中。程序运行时,完全不依赖静态库文件。- 动态库 :Linux 下后缀是
.so,Windows 下是.dll。程序编译时只记录函数入口地址,运行时才去加载库文件并调用函数。多个程序可以共享同一个动态库,实现 "一份代码,多处使用"。

我们可以用ls命令直观查看系统中的动静态库。比如 Linux 系统下的 C 标准库:
bash
# 查看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
# 查看C标准静态库
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.a
C++ 标准库同理,动态库通常是软链接形式,指向具体版本:
bash
ls -l /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so
# 输出: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
接下来,我们用一个自定义的mystdio库(模拟标准 IO 功能)作为案例,一步步演示动静态库的制作和使用。
二、静态库 ------"打包带走" 的代码
静态库的核心特点是编译时嵌入,运行时独立。就像你点外卖时把餐具打包带走,吃饭的时候不需要再回餐馆拿 ------ 程序一旦编译完成,静态库就可以删除,完全不影响运行。
2.1 准备源码 ------ 造一个 "迷你标准库"
我们先写两个基础文件:my_stdio.h(头文件,声明函数)和my_stdio.c(源文件,实现函数),再加上一个字符串处理文件my_string.c。
2.1.1 头文件:my_stdio.h
这个头文件定义了模拟的文件结构体和 IO 函数,类似标准库的FILE和fopen、fwrite等函数。
cpp
#pragma once
#define SIZE 1024
#define FLUSH_NONE 0
#define FLUSH_LINE 1
#define FLUSH_FULL 2
// 模拟标准库的FILE结构体
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);
2.1.2 源文件:my_stdio.c
实现文件的打开、写入、刷新和关闭功能,底层调用 Linux 系统调用open、write等。
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>
// 打开文件,模拟fopen
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;
}
// 刷新缓冲区,模拟fflush
void mfflush(mFILE *stream)
{
if (stream->size > 0)
{
write(stream->fileno, stream->outbuffer, stream->size);
fsync(stream->fileno); // 强制写入磁盘
stream->size = 0;
}
}
// 写入数据,模拟fwrite
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;
}
// 关闭文件,模拟fclose
void mfclose(mFILE *stream)
{
if (stream->size > 0)
{
mfflush(stream);
}
close(stream->fileno);
free(stream);
}
2.1.3 字符串处理:my_string.c
实现一个简单的my_strlen函数,模拟标准库的strlen。
cpp
#include "my_string.h"
// 计算字符串长度
int my_strlen(const char *s)
{
const char *end = s;
while (*end != '\0') end++;
return end - s;
}
对应的头文件my_string.h:
cpp
#pragma once
int my_strlen(const char *s);
2.2 制作静态库 ------ 把代码 "打包" 成.a 文件
制作静态库的步骤很简单:编译生成.o 目标文件 → 用 ar 工具打包成.a 文件。
我们可以写一个**Makefile**来自动化这个过程,避免手动敲命令。
2.2.1 编写 Makefile
bash
# 目标:静态库libmystdio.a
libmystdio.a: my_stdio.o my_string.o
@ar -rc $@ $^
@echo "build $^ to $@ ... done"
# 编译生成.o文件(-c表示只编译不链接)
%.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"
2.2.2 关键命令解释
- *gcc -c .c:把
.c文件编译成.o目标文件,不进行链接。- ar -rc libmystdio.a my_stdio.o my_string.o:
ar是 GNU 归档工具,rc表示 "创建并替换"。libmystdio.a是静态库名,必须以lib开头,后缀是.a。- ar -tv libmystdio.a:查看静态库中的文件列表,
t是列出内容,v是显示详细信息。
执行**make**命令,就能生成libmystdio.a静态库:
cpp
make
# 输出:
# compling my_stdio.c to my_stdio.o ... done
# compling my_string.c to my_string.o ... done
# build my_stdio.o my_string.o to libmystdio.a ... done
2.3 使用静态库 ------ 链接成可执行程序
静态库制作完成后,我们写一个main.c来测试它的功能。
2.3.1 测试程序:main.c
cpp
#include "my_stdio.h"
#include "my_string.h"
#include <stdio.h>
int main()
{
const char *s = "hello static library!\n";
// 测试my_strlen
printf("%s: length = %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;
}
2.3.2 编译链接 ------ 三种场景
链接静态库的核心是告诉编译器头文件在哪 、库文件在哪 、库名是什么。对应的 gcc 参数是:
- -I:指定头文件搜索路径
- -L:指定库文件搜索路径
- -l:指定库名(去掉
lib前缀和.a后缀)
场景 1:头文件和库文件在系统路径
如果把my_stdio.h、my_string.h拷贝到/usr/include,把libmystdio.a拷贝到/usr/lib,可以直接编译:
bash
gcc main.c -lmystdio -o main
-lmystdio就是链接libmystdio.a,编译器会自动去系统路径找。
场景 2:头文件和库文件在当前目录
头文件、库文件和main.c在同一文件夹下:
bash
gcc main.c -L. -lmystdio -o main
**-L.**表示库文件在当前目录(.代表当前路径)。
场景 3:头文件和库文件在自定义路径
如果头文件在./include,库文件在./lib:
bash
gcc main.c -I./include -L./lib -lmystdio -o main
2.3.3 验证静态库的独立性
编译生成可执行文件main后,我们删除静态库,再运行程序:
bash
rm -f libmystdio.a
./main
# 输出:
# hello static library!
# : length = 22
cat log.txt
# 输出3行hello static library!
程序正常运行!这就是静态库的优势 ------编译后脱离库文件,可独立运行。
2.4 静态库的优缺点 ------ 优点缺点同样明显
优点:
- 运行独立:可执行文件包含了库的代码,不需要依赖外部文件。
- 运行速度快:没有运行时的动态链接开销,执行效率高。
- 部署方便:发给别人时只需要一个可执行文件,不需要附带库。
缺点:
- 可执行文件体积大 :每个程序都会复制一份库代码,多个程序会造成冗余。比如 10 个程序都用
libmystdio.a,就会有 10 份相同的代码。- 更新麻烦:如果库有 bug 需要修复,所有使用该库的程序都要重新编译链接。
三、动态库 ------"共享使用" 的代码
动态库的核心特点是编译时记录地址,运行时加载共享。就像你去餐馆吃饭,餐具是餐馆提供的,多个顾客共享同一套餐具 ------ 多个程序可以共享同一个动态库,大大节省内存和磁盘空间。
3.1 制作动态库 ------ 生成.so 文件
动态库的制作和静态库类似,但需要两个关键参数:-fPIC和-shared。
-fPIC:生成位置无关代码(Position Independent Code)。动态库加载到内存的地址是不固定的,PIC 保证代码在任意地址都能正常运行。-shared:生成共享库(动态库)。
3.1.1 编写 Makefile
bash
# 目标:动态库libmystdio.so
libmystdio.so: my_stdio.o my_string.o
gcc -o $@ $^ -shared
@echo "build $^ to $@ ... done"
# 编译生成位置无关的.o文件
%.o: %.c
gcc -fPIC -c $<
@echo "compling $< to $@ ... done"
# 清理
.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"
执行**make**命令,生成libmystdio.so动态库:
bash
make
# 输出:
# compling my_stdio.c to my_stdio.o ... done
# compling my_string.c to my_string.o ... done
# build my_stdio.o my_string.o to libmystdio.so ... done
3.2 使用动态库 ------ 编译链接 + 运行时加载
动态库的编译链接命令和静态库完全一样,但运行时需要让系统找到动态库文件。
3.2.1 编译测试程序
还是用之前的main.c,编译命令:
bash
gcc main.c -I. -L. -lmystdio -o main
编译成功后,直接运行./main会报错:
bash
./main
# 输出:
# ./main: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory
原因是系统找不到动态库文件 。Linux 下动态库的搜索路径是有优先级的,我们需要把libmystdio.so加入搜索路径。
3.2.2 解决动态库找不到的问题 ------ 四种方案
方案 1:拷贝动态库到系统路径
把libmystdio.so拷贝到/usr/lib或/lib64(系统默认搜索路径):
bash
sudo cp libmystdio.so /usr/lib
./main
# 正常运行
方案 2:设置环境变量 LD_LIBRARY_PATH
临时添加库路径,关闭终端后失效:
bash
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
./main
# 正常运行
如果想永久生效,可以把这行命令写入~/.bashrc或~/.zshrc,然后source ~/.bashrc。
方案 3:添加软链接到系统路径
和方案 1 类似,但用软链接,方便更新库:
bash
sudo ln -s $(pwd)/libmystdio.so /usr/lib/libmystdio.so
方案 4:配置 /etc/ld.so.conf.d
创建自定义配置文件,添加库路径:
bash
sudo echo $(pwd) > /etc/ld.so.conf.d/mystdio.conf
sudo ldconfig # 更新缓存
这种方式适合长期使用的自定义库。
3.2.3 查看程序依赖的动态库
用**ldd**命令可以查看可执行程序依赖的所有动态库:
bash
ldd main
# 输出:
# linux-vdso.so.1 => (0x00007fffacbbf000)
# libmystdio.so => ./libmystdio.so (0x00007f8917335000)
# libc.so.6 => /lib64/libc.so.6 (0x00007f8916f67000)
# /lib64/ld-linux-x86-64.so.2 (0x00007f8917905000)
可以看到main依赖libmystdio.so和系统的libc.so.6。
3.3 动态库的优缺点 ------ 空间换时间的典范
优点:
- 节省空间:多个程序共享一个动态库,磁盘和内存占用大大减少。比如 10 个程序用同一个动态库,内存中只需要一份库代码。
- 更新方便:修复库的 bug 后,不需要重新编译程序,直接替换动态库文件即可。
- 版本灵活:可以同时存在多个版本的动态库,满足不同程序的需求。
缺点:
- 运行依赖库:程序运行时必须找到对应的动态库,否则无法启动。
- 运行时有开销:动态链接需要在运行时解析函数地址,比静态库多了一点性能损耗(现代系统中这个损耗可以忽略不计)。
- 部署麻烦:发给别人时需要附带动态库文件,或者配置库路径。
四、动静态库的核心区别 ------ 底层逻辑大揭秘
看到这里,你可能已经会用动静态库了,但还想知道为什么静态库编译后不依赖,动态库运行时才加载 ?这就需要从链接过程 和内存加载两个层面来理解。
4.1 链接过程的区别 ------ 静态链接是 "复制",动态链接是 "记录"
- 静态链接 :编译器在链接阶段,会把静态库中用到的函数代码直接复制到可执行文件中。链接完成后,可执行文件包含了所有需要的代码,和静态库彻底没关系。
- 动态链接 :编译器在链接阶段,不复制代码,只在可执行文件中记录函数的 "入口地址" 和 "依赖的动态库名"。程序运行时,操作系统的动态链接器会根据这些信息,找到并加载动态库,然后把函数地址替换成实际的内存地址。
4.2 内存加载的区别 ------ 静态库是 "私有",动态库是 "共享"
- 静态库:每个程序加载时,都会把自己的可执行文件(包含静态库代码)加载到内存,不同程序的静态库代码是独立的,互不干扰。
- 动态库 :动态库被加载到内存后,会被所有依赖它的程序共享。操作系统用虚拟内存机制,让多个进程映射到同一份物理内存的动态库代码,大大节省内存。
4.3 直观对比 ------ 一张表看懂区别
| 特性 | 静态库(.a) | 动态库(.so) |
|---|---|---|
| 链接方式 | 编译时复制代码到可执行文件 | 编译时记录地址,运行时解析 |
| 运行依赖 | 不依赖库文件,可独立运行 | 必须依赖库文件,否则无法启动 |
| 可执行文件体积 | 大(包含库代码) | 小(不包含库代码) |
| 内存占用 | 高(每个程序一份代码) | 低(多个程序共享一份代码) |
| 更新维护 | 需重新编译程序 | 直接替换库文件即可 |
| 性能 | 略高(无运行时链接开销) | 略低(有运行时链接开销) |
五、拓展:试试 ncurses 图形库
最后给大家推荐一个好玩的图形库 ------ncurses,它可以用来制作终端界面的图形程序,比如进度条、菜单、游戏等。
安装 ncurses 库:
bash
# CentOS
sudo yum install -y ncurses-devel
# Ubuntu
sudo apt install -y libncurses-dev
这里推荐一篇不错的使用指南:https://blog.csdn.net/bdn_nbd/article/details/134019142
感兴趣的同学可以自己试试写一个简单的进度条程序来体验一下。

总结
库是代码复用的核心,也是大型项目模块化开发的基础。掌握动静态库的使用,是 C/C++ 程序员从 "入门" 到 "进阶" 的必经之路。希望你能把今天学到的知识用到实际开发中,写出更优雅的代码~
到这里,动静态库的原理和使用就讲完了。从源码到制作,从使用到底层逻辑,希望这篇文章能帮你彻底搞懂动静态库。如果觉得有用,欢迎点赞、收藏、转发!有问题欢迎在评论区交流~
