14、库与归档文件:静态库(.a)的创建与使用,ar工具

说到静态库,我脑子里第一个蹦出来的场景是十年前做嵌入式项目那会儿。那时候内存金贵得很,每次链接都恨不得把每个字节都算清楚。静态库这东西,说白了就是把一堆目标文件打包成一个文件,方便别人用,也方便自己管理。

你可能会问:为什么不直接用源代码?嗯,这里有个现实问题——你写了个加密算法库,总不想把源码直接给别人吧?静态库就是干这个的。它把编译好的机器码打包起来,只暴露头文件里的接口声明。

静态库的本质

静态库在Linux下是.a文件,Windows下是.lib。但本质上,它们都是归档文件。什么叫归档?就是一堆文件的集合,附带一个索引表。

我用ar工具创建静态库时,经常跟团队里新人说:ar就像是一个没有压缩的zip打包器。它把多个.o文件塞进一个.a文件里,链接器再用的时候,从里面挑需要的目标文件。

核心要点:静态库不是可执行文件,也不是目标文件。它就是一个档案柜,里面整整齐齐摆着目标文件。

创建静态库:ar工具实战

先看一个最简单的例子。假设我有两个模块:math_ops.cstring_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.ostring_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只用了addmultiply,那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.c math_ops.c string_ops.c gcc -c main.o math_ops.o string_ops.o ar rcs libutils.a math_ops.o + string_ops.o 链接器 (ld) test (可执行文件) 链接器只提取 需要的 .o 文件 string_ops.o 被忽略

从图上你能看到:main.o直接参与链接,而libutils.a里的目标文件,链接器只挑需要的。这就是为什么静态库能节省编译时间——你改了一个模块,只需要重新编译那个模块,然后更新库就行。

静态库的优缺点

用了这么多年静态库,我总结了几点:

优点:

  • 部署简单,一个可执行文件搞定,不用带一堆动态库
  • 运行速度快,没有动态链接的开销
  • 版本管理容易,不会出现"DLL地狱"

缺点:

  • 可执行文件体积大,每个程序都拷贝一份库代码
  • 更新库需要重新链接所有依赖的程序
  • 内存浪费,多个进程用同一个库,内存里有多份拷贝

避坑指南:我曾经在一个项目里,把静态库和动态库混着用,结果链接时符号冲突,折腾了两天才找到原因。记住:同一个符号不要同时出现在静态库和动态库里,否则链接器会随机选一个,行为不可预测。

静态库的命名与搜索路径

Linux下静态库的命名规则是lib[name].a。链接器搜索库的顺序是:

  1. -L指定的路径(按出现顺序)
  2. LIBRARY_PATH环境变量
  3. 系统默认路径:/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