14、库与归档文件:静态库(.a)的创建与使用,ar工具
说到静态库,我脑子里第一个蹦出来的场景是十年前做嵌入式项目那会儿。那时候内存金贵得很,每次链接都恨不得把每个字节都算清楚。静态库这东西,说白了就是把一堆目标文件打包成一个文件,方便别人用,也方便自己管理。
你可能会问:为什么不直接用源代码?嗯,这里有个现实问题——你写了个加密算法库,总不想把源码直接给别人吧?静态库就是干这个的。它把编译好的机器码打包起来,只暴露头文件里的接口声明。
静态库的本质
静态库在Linux下是.a文件,Windows下是.lib。但本质上,它们都是归档文件。什么叫归档?就是一堆文件的集合,附带一个索引表。
我用ar工具创建静态库时,经常跟团队里新人说:ar就像是一个没有压缩的zip打包器。它把多个.o文件塞进一个.a文件里,链接器再用的时候,从里面挑需要的目标文件。
核心要点:静态库不是可执行文件,也不是目标文件。它就是一个档案柜,里面整整齐齐摆着目标文件。
创建静态库:ar工具实战
先看一个最简单的例子。假设我有两个模块:math_ops.c和string_ops.c。
// math_ops.c
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
// string_ops.c
#include <string.h>
void copy_str(char *dest, const char *src) {
strcpy(dest, src);
}
int str_len(const char *s) {
return strlen(s);
}
第一步,编译成目标文件:
gcc -c math_ops.c string_ops.c
这一步会生成math_ops.o和string_ops.o。我记得刚入行时总搞混-c和-o,其实-c就是"只编译不链接"。
第二步,用ar打包:
ar rcs libutils.a math_ops.o string_ops.o
这里rcs三个参数,我一个个说:
- r:替换。如果库里有同名文件,就替换掉
- c:创建。如果库不存在,就新建一个
- s:建立索引。这个很重要,没有索引链接器会找不到符号
个人习惯:我每次创建静态库都会加s参数。曾经有一次忘了加,链接时疯狂报"undefined reference",排查了半天才发现是索引没建。后来我干脆写了个Makefile模板,自动加s。
使用静态库
现在库有了,怎么用?写个测试程序:
// main.c
#include <stdio.h>
int add(int, int);
int multiply(int, int);
int main() {
printf("3 + 5 = %d\n", add(3, 5));
printf("3 * 5 = %d\n", multiply(3, 5));
return 0;
}
编译链接:
gcc main.c -L. -lutils -o test
-L.告诉链接器在当前目录找库,-lutils告诉它链接libutils.a。注意命名规则:lib前缀和.a后缀可以省略。
运行一下:
$ ./test
3 + 5 = 8
3 * 5 = 15
嗯,一切正常。但这里有个细节——链接器只提取需要的目标文件。比如main.c只用了add和multiply,那string_ops.o就不会被链接进最终的可执行文件。
ar工具的更多用法
除了创建,ar还能干不少事。我整理了一张表:
| 命令 | 作用 | 示例 |
|---|---|---|
ar t lib.a |
查看库里的文件列表 | ar t libutils.a |
ar x lib.a |
从库里提取文件 | ar x libutils.a |
ar d lib.a file.o |
删除库里的某个文件 | ar d libutils.a string_ops.o |
ar r lib.a file.o |
向库里添加文件 | ar r libutils.a new_ops.o |
我曾经在维护一个老项目时,需要替换库里的某个模块。当时没重新编译整个库,直接用ar d删掉旧的目标文件,再用ar r加进去新的。省了不少编译时间。
静态库的链接过程
这里画个图,帮你理解链接器是怎么处理静态库的:
从图上你能看到:main.o直接参与链接,而libutils.a里的目标文件,链接器只挑需要的。这就是为什么静态库能节省编译时间——你改了一个模块,只需要重新编译那个模块,然后更新库就行。
静态库的优缺点
用了这么多年静态库,我总结了几点:
优点:
- 部署简单,一个可执行文件搞定,不用带一堆动态库
- 运行速度快,没有动态链接的开销
- 版本管理容易,不会出现"DLL地狱"
缺点:
- 可执行文件体积大,每个程序都拷贝一份库代码
- 更新库需要重新链接所有依赖的程序
- 内存浪费,多个进程用同一个库,内存里有多份拷贝
避坑指南:我曾经在一个项目里,把静态库和动态库混着用,结果链接时符号冲突,折腾了两天才找到原因。记住:同一个符号不要同时出现在静态库和动态库里,否则链接器会随机选一个,行为不可预测。
静态库的命名与搜索路径
Linux下静态库的命名规则是lib[name].a。链接器搜索库的顺序是:
-L指定的路径(按出现顺序)LIBRARY_PATH环境变量- 系统默认路径:
/lib、/usr/lib、/usr/local/lib
我建议你在项目里统一用-L指定路径,别依赖环境变量。环境变量这东西,换台机器就变了,到时候链接失败,排查起来很头疼。
实战:一个完整的Makefile示例
最后,给你看一个我常用的Makefile模板:
CC = gcc
AR = ar
CFLAGS = -Wall -O2
LIB = libutils.a
# 目标文件
OBJS = math_ops.o string_ops.o
# 默认目标
all: test
# 创建静态库
$(LIB): $(OBJS)
$(AR) rcs $@ $^
# 编译目标文件
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# 链接可执行文件
test: main.o $(LIB)
$(CC) main.o -L. -lutils -o test
# 清理
clean:
rm -f *.o *.a test
.PHONY: all clean
这个模板我用了好多年,改改就能用。注意$(AR) rcs $@ $^这行,$@是目标文件(libutils.a),$^是所有依赖(math_ops.o string_ops.o)。
静态库这东西,说简单也简单,说复杂也复杂。但只要你理解了它的本质——一个带索引的目标文件集合——大部分问题都能迎刃而解。嗯,今天就聊到这儿,你回去可以自己动手试试,把几个小模块打包成静态库,感受一下。
最后提醒:用nm libutils.a可以查看静态库里的符号表。如果链接时遇到"undefined reference",先用nm确认一下你的符号是不是真的在库里。这个习惯帮我省了不少调试时间。
公众号:蓝海资料掘金营,微信deep3321