目录
一、动静态库的本质与编译流程
1.1 程序的诞生过程
从源代码到可执行程序需经历四个关键阶段:
# 预处理(展开头文件/宏替换)
gcc -E test.c -o test.i
# 编译(生成汇编代码)
gcc -S test.i -o test.s
# 汇编(生成机器码)
gcc -c test.s -o test.o
# 链接(组合目标文件)
gcc test.o -o test
1.2 库的定位
动静态库本质是目标文件的集合,是软件复用的重要手段。当多个项目需要相同功能模块时:
test1.c、test2.c 等公共代码编译为目标文件
打包目标文件形成库文件
新项目只需链接库文件即可复用功能
例如,用test1.c
、test2.c
、test3.c
、test4.c
和 main1.c
生成可执行文件时,需要先生成各个文件的目标文件(test1.o
、test2.o
等),再将它们链接起来。如果这些文件在多个项目中频繁使用,可以将它们的目标文件打包成一个库,供其他项目直接链接使用。
二、动静态库的直观对比
在Linux下,我们可以通过以下代码和命令来认识动静态库:
2.1 示例代码
#include <stdio.h>
int main()
{
printf("hello world\n");
return 0;
}
编译并运行该代码:
2.2 查看可执行程序依赖的库
使用
ldd
命令查看可执行程序依赖的库文件:
ldd test
输出结果中会包含
libc.so.6
,这是C标准库的动态库,我们通过ls命令还可以发现libc.so.6
实际上只是一个软链接。
实际上该软链接的源文件
libc-2.31.so
和libc.so.6
在同一个目录下,为了进一步了解,我们可以通过file 文件名
命令来查看libc-2.31.so
的文件类型。
可以看到,
libc.so.6
是一个动态库(共享库)。
2.3 动静态库的区别
-
动态库:以
.so
为后缀(Linux)或.dll
为后缀(Windows)。程序运行时动态链接库代码,多个程序共享同一份库代码。 -
静态库:以
.a
为后缀(Linux)或.lib
为后缀(Windows)。程序编译时将库代码复制到可执行文件中,运行时不再依赖库。
2.4 静态链接与动态链接
默认情况下,
gcc
编译器使用动态链接。若要进行静态链接,可以使用-static
选项:
gcc -o test test.c -static
静态链接生成的可执行程序文件较大,但不依赖外部库。此时当我们使用
ldd 文件名
命令查看该可执行程序所依赖的库文件时就会看到以下信息:
三、动静态库各自的特征
特性 | 静态库 | 动态库 |
---|---|---|
链接时机 | 编译时 | 运行时 |
文件包含 | 完整代码拷贝 | 符号表引用 |
内存占用 | 独立副本 | 共享内存映射 |
更新维护 | 需重新编译 | 热替换更新 |
典型文件后缀 | .a (Linux), .lib (Windows) | .so (Linux), .dll (Windows) |
3.1 静态库
由于静态库的代码是直接复制到可执行文件中的,所以生成的可执行文件会比较大,占用更多的磁盘空间。但这也意味着, 可执行文件在运行时不再需要外部的库支持,可以单独运行,完全不依赖其他文件,就好比是一个“全能型”的独立个体,自带所有需要的工具和资源。
优点:生成的可执行程序独立运行,不依赖外部库。
缺点:可执行程序体积较大,多个程序使用相同库时会导致内存中存在重复代码。
3.2 动态库
动态库就像是一家饭店,饭店的菜单上写着各种菜名(相当于函数入口地址表),但只有等客人点菜了,厨师才会做这道菜(操作系统动态加载动态库代码)。这样,饭店不用把所有的菜都提前做好存在厨房里(节省磁盘空间),而且很多客人点同一道菜,厨师只需要做一份,然后给需要的客人盛到各自的盘子里(多个程序共享动态库)。这种方式既减少了厨房食材的存储空间(节省磁盘空间),也避免了同一道菜在多个盘子里重复摆放浪费桌子空间(节省内存,因为物理内存中只有一份动态库实例)。
优点:节省磁盘空间和内存,多个程序共享同一份库代码。
缺点:必须依赖动态库,否则程序无法运行。
四、静态库的打包与使用
4.1 示例代码
假设我们有以下文件:
-
add.h
和sub.h
(头文件) -
add.c
和sub.c
(源文件)
头文件内容
add.h
:
#pragma once
extern int add(int x, int y);
sub.h
:
#pragma once
extern int sub(int x, int y);
源文件内容
add.c
:
#include "add.h"
int add(int x, int y)
{
return x + y;
}
sub.c
:
#include "sub.h"
int sub(int x, int y)
{
return x - y;
}
4.2 打包静态库
1. 生成目标文件
使用以下命令将源文件 add.c
和 sub.c
编译为目标文件:
gcc -c add.c sub.c
执行此命令后,编译器会生成两个目标文件:add.o
和 sub.o
。这些 .o
文件是编译后的二进制目标文件,包含了源文件中函数的汇编代码,但尚未最终链接为可执行文件。
2. 生成静态库
使用以下命令将生成的目标文件打包为静态库:
ar -rc libcal.a add.o sub.o
-
-r
: 表示插入目标文件,如果库中已存在同名的文件,会自动替换。 -
-c
: 表示如果库文件不存在,则创建一个新库。 -
libcal.a
: 是生成的静态库文件的名称。 打包完成后,libcal.a
静态库文件便创建成功。
3. 查看静态库内容
使用以下命令可以查看静态库文件的内容:
ar -tv libcal.a
-
-t
: 表示列出库中的文件。 -
-v
: 表示以详细模式显示文件信息。 执行命令后,会显示库中包含的目标文件的详细信息,如文件名、修改时间、所有者、权限、文件大小等。
4. 组织头文件和库文件
将相关的头文件和库文件整理到指定的目录中,最终的文件夹结构如下:
-
include/
目录用于存放头文件,如add.h
和sub.h
。这些头文件包含了函数的声明,供其他代码文件引用。 -
lib/
目录用于存放生成的静态库文件libcal.a
。该库文件包含了编译后的目标代码,供链接器在链接阶段使用。
4.3 使用静态库
1. 创建主程序 main.c
创建一个名为 main.c
的文件,该文件使用了我们之前生成的静态库中的函数。以下是 main.c
的内容:
#include <stdio.h>
#include "add.h" // 包含自定义函数的头文件
int main() {
int x = 20, y = 10, z = add(x, y); // 调用 `add` 函数计算两数之和
printf("%d + %d = %d\n", x, y, z); // 输出计算结果
return 0;
}
2. 方法一:使用编译选项
使用以下命令将 main.c
编译成可执行文件 main
:
gcc main.c -I./mathlib/include -L./mathlib/lib -lcal -o main
解释:
-
-I./mathlib/include
: 指定头文件搜索路径为mathlib/include
,编译器会在此目录下查找add.h
文件。 -
-L./mathlib/lib
: 指定库文件搜索路径为mathlib/lib
,链接器会在此目录下查找libcal.a
静态库文件。 -
-lcal
: 链接名为libcal.a
的静态库文件(-l
参数表示链接库文件,cal
是库文件的简称,编译器会自动补全为libcal.a
)。 -
-o main
: 指定生成的可执行文件名为main
。
3. 方法二:将头文件和库文件拷贝到系统路径
如果希望在系统中任何地方都能直接使用该库,可以将头文件和库文件拷贝到系统的搜索路径,例如:
# 拷贝头文件到 `/usr/include`
sudo cp ./mathlib/include/add.h /usr/include/
sudo cp ./mathlib/include/sub.h /usr/include/
# 拷贝库文件到 `/usr/lib`
sudo cp ./mathlib/lib/libcal.a /usr/lib/
后续编译命令: 拷贝完成后,无需指定 -I
和 -L
路径,直接使用以下命令编译即可:
gcc main.c -lcal -o main
解释:
-
/usr/include
是系统默认的头文件搜索路径。 -
/usr/lib
是系统默认的库文件搜索路径。
总结:
-
方法一 是一种更灵活的方式,适合在一个项目中使用,并且不修改系统文件。
-
方法二 是一种全局的方式,适合将库文件提供给系统中的多个项目使用,但需要谨慎操作,避免冲突。
五、动态库的打包与使用
5.1 打包动态库
1. 生成位置无关的对象文件
启用 -fPIC
选项编译源文件,生成位置无关代码的目标文件:
gcc -fPIC -c add.c sub.c
结果会生成 add.o
和 sub.o
两个位置无关代码的目标文件。
2. 生成动态库
使用以下命令将目标文件链接为共享动态库:
gcc -shared -o libcal.so add.o sub.o
-shared
选项用于生成共享库文件,-o libcal.so
指定生成的动态库文件名为libcal.so
。
3. 组织头文件和库文件
将相关文件放入指定目录,最终的文件夹结构如下:
include/
目录存放头文件add.h
和sub.h
。
lib/
目录存放生成的动态库文件libcal.so
。
5.2 使用动态库
1. 创建主程序 main.c
(同前)
#include <stdio.h>
#include "add.h"
int main() {
int x = 20, y = 10, z = add(x, y);
printf("%d + %d = %d\n", x, y, z);
return 0;
}
2. 使用编译选项
编译命令如下:
gcc main.c -I./mlib/include -L./mlib/lib -lcal -o main
参数解释:
-
-I./mlib/include
: 指定头文件搜索路径为mlib/include
。 -
-L./mlib/lib
: 指定库文件搜索路径为mlib/lib
。 -
-lcal
: 链接动态库libcal.so
。 -
-o main
: 指定生成的可执行文件名为main
。
但是与静态库的使用不同的是,此时我们生成的可执行程序并不能直接运行。
因为此时可执行程序所依赖的动态库是没有被找到的,我们可以使用ldd
命令进行查看。
3. 解决动态库运行时依赖问题
方式一:将动态库拷贝到系统路径
将动态库文件拷贝到系统的共享库路径(如 /lib64
或 /usr/lib64
):
sudo cp mlib/lib/libcal.so /lib64/
注意事项:
-
使用
/lib64
或/usr/lib64
需要管理员权限。 -
不建议随意改动系统库路径,可能会引发系统兼容性问题。
方式二:设置
LD_LIBRARY_PATH
在当前终端会话中,通过以下命令设置动态库路径:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/mlib/lib
或将路径永久添加到环境变量(编辑 ~/.bashrc
或 ~/.bash_profile
文件):
echo 'export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/path/to/mlib/lib' >> ~/.bashrc
source ~/.bashrc
方式三:更新动态链接器缓存
创建自定义的动态链接器配置文件(例如 /etc/ld.so.conf.d/mylib.conf
):
echo "/path/to/mlib/lib" > /etc/ld.so.conf.d/mylib.conf
sudo ldconfig
步骤:
-
创建
/etc/ld.so.conf.d/mylib.conf
文件,内容为/path/to/mlib/lib
。 -
运行
sudo ldconfig
更新动态链接器缓存。
总结
1. 动态库需要在运行时加载,因此需要确保动态库路径设置正确。
2. 方法一(使用 -I
和 -L
编译选项)适用于开发和测试阶段。
3. 方法二(设置动态库路径)适用于部署环境,其中:
-
拷贝到系统库路径是最直接的方式,但需谨慎操作。
-
设置
LD_LIBRARY_PATH
是临时且灵活的方式。 -
更新动态链接器缓存是最规范的方式,适合长期使用。