
在 C/C++ 开发的世界里,库就像是程序员的 “神兵利器”。它把成熟、可复用的代码打包成二进制文件,让我们不用重复造轮子,直接站在巨人的肩膀上开发。你写的每一个
printf、每一次字符串操作,其实都在和库打交道。今天我们就彻底撕开动静态库的神秘面纱,从原理剖析到实战操作,手把手带你搞懂它们的区别、制作方法和底层逻辑。下面就让我们正式开始吧!
首先我们要明确:库是可复用代码的二进制形式,能被操作系统载入内存执行。它就像乐高积木,不同的积木块(库函数)可以组合出各种复杂的程序(模型)。
从类型上看,库主要分为两大类:
.a,Windows 下是.lib。程序编译链接时,会把库的代码直接复制到可执行文件中。程序运行时,完全不依赖静态库文件。.so,Windows 下是.dll。程序编译时只记录函数入口地址,运行时才去加载库文件并调用函数。多个程序可以共享同一个动态库,实现 “一份代码,多处使用”。
我们可以用ls命令直观查看系统中的动静态库。比如 Linux 系统下的 C 标准库:
# 查看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.aC++ 标准库同理,动态库通常是软链接形式,指向具体版本:
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 功能)作为案例,一步步演示动静态库的制作和使用。
静态库的核心特点是编译时嵌入,运行时独立。就像你点外卖时把餐具打包带走,吃饭的时候不需要再回餐馆拿 —— 程序一旦编译完成,静态库就可以删除,完全不影响运行。
我们先写两个基础文件:my_stdio.h(头文件,声明函数)和my_stdio.c(源文件,实现函数),再加上一个字符串处理文件my_string.c。
这个头文件定义了模拟的文件结构体和 IO 函数,类似标准库的FILE和fopen、fwrite等函数。
#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); 实现文件的打开、写入、刷新和关闭功能,底层调用 Linux 系统调用open、write等。
#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);
} 实现一个简单的my_strlen函数,模拟标准库的strlen。
#include "my_string.h"
// 计算字符串长度
int my_strlen(const char *s)
{
const char *end = s;
while (*end != '\0') end++;
return end - s;
} 对应的头文件my_string.h:
#pragma once
int my_strlen(const char *s);制作静态库的步骤很简单:编译生成.o 目标文件 → 用 ar 工具打包成.a 文件。
我们可以写一个Makefile来自动化这个过程,避免手动敲命令。
# 目标:静态库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"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静态库:
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 静态库制作完成后,我们写一个main.c来测试它的功能。
#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;
}链接静态库的核心是告诉编译器头文件在哪、库文件在哪、库名是什么。对应的 gcc 参数是:
-I:指定头文件搜索路径-L:指定库文件搜索路径-l:指定库名(去掉lib前缀和.a后缀) 如果把my_stdio.h、my_string.h拷贝到/usr/include,把libmystdio.a拷贝到/usr/lib,可以直接编译:
gcc main.c -lmystdio -o main -lmystdio就是链接libmystdio.a,编译器会自动去系统路径找。
头文件、库文件和main.c在同一文件夹下:
gcc main.c -L. -lmystdio -o main -L.表示库文件在当前目录(.代表当前路径)。
如果头文件在./include,库文件在./lib:
gcc main.c -I./include -L./lib -lmystdio -o main 编译生成可执行文件main后,我们删除静态库,再运行程序:
rm -f libmystdio.a
./main
# 输出:
# hello static library!
# : length = 22
cat log.txt
# 输出3行hello static library!程序正常运行!这就是静态库的优势 ——编译后脱离库文件,可独立运行。
优点:
缺点:
libmystdio.a,就会有 10 份相同的代码。动态库的核心特点是编译时记录地址,运行时加载共享。就像你去餐馆吃饭,餐具是餐馆提供的,多个顾客共享同一套餐具 —— 多个程序可以共享同一个动态库,大大节省内存和磁盘空间。
动态库的制作和静态库类似,但需要两个关键参数:-fPIC和-shared。
-fPIC:生成位置无关代码(Position Independent Code)。动态库加载到内存的地址是不固定的,PIC 保证代码在任意地址都能正常运行。-shared:生成共享库(动态库)。# 目标:动态库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动态库:
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动态库的编译链接命令和静态库完全一样,但运行时需要让系统找到动态库文件。
还是用之前的main.c,编译命令:
gcc main.c -I. -L. -lmystdio -o main 编译成功后,直接运行./main会报错:
./main
# 输出:
# ./main: error while loading shared libraries: libmystdio.so: cannot open shared object file: No such file or directory 原因是系统找不到动态库文件。Linux 下动态库的搜索路径是有优先级的,我们需要把libmystdio.so加入搜索路径。
把libmystdio.so拷贝到/usr/lib或/lib64(系统默认搜索路径):
sudo cp libmystdio.so /usr/lib
./main
# 正常运行临时添加库路径,关闭终端后失效:
export LD_LIBRARY_PATH=./:$LD_LIBRARY_PATH
./main
# 正常运行 如果想永久生效,可以把这行命令写入~/.bashrc或~/.zshrc,然后source ~/.bashrc。
和方案 1 类似,但用软链接,方便更新库:
sudo ln -s $(pwd)/libmystdio.so /usr/lib/libmystdio.so创建自定义配置文件,添加库路径:
sudo echo $(pwd) > /etc/ld.so.conf.d/mystdio.conf
sudo ldconfig # 更新缓存这种方式适合长期使用的自定义库。
用ldd命令可以查看可执行程序依赖的所有动态库:
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。
优点:
缺点:
看到这里,你可能已经会用动静态库了,但还想知道为什么静态库编译后不依赖,动态库运行时才加载?这就需要从链接过程和内存加载两个层面来理解。
特性 | 静态库(.a) | 动态库(.so) |
|---|---|---|
链接方式 | 编译时复制代码到可执行文件 | 编译时记录地址,运行时解析 |
运行依赖 | 不依赖库文件,可独立运行 | 必须依赖库文件,否则无法启动 |
可执行文件体积 | 大(包含库代码) | 小(不包含库代码) |
内存占用 | 高(每个程序一份代码) | 低(多个程序共享一份代码) |
更新维护 | 需重新编译程序 | 直接替换库文件即可 |
性能 | 略高(无运行时链接开销) | 略低(有运行时链接开销) |
最后给大家推荐一个好玩的图形库 ——ncurses,它可以用来制作终端界面的图形程序,比如进度条、菜单、游戏等。
安装 ncurses 库:
# 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++ 程序员从 “入门” 到 “进阶” 的必经之路。希望你能把今天学到的知识用到实际开发中,写出更优雅的代码~ 到这里,动静态库的原理和使用就讲完了。从源码到制作,从使用到底层逻辑,希望这篇文章能帮你彻底搞懂动静态库。如果觉得有用,欢迎点赞、收藏、转发!有问题欢迎在评论区交流~