哎呦 资料合集
链接:https://pan.quark.cn/s/770d9387db5f

在软件开发的世界里,我们很少从零开始构建一切。“不要重复造轮子”是程序员的信条之一。库(Library)正是这一信条的最佳实践,它将可重用的代码打包,供多个程序使用。在C/C++领域,库主要分为两种形式:静态库(Static Libraries)和动态库(Dynamic Libraries)。

它们的工作机制、优缺点和适用场景有天壤之别。本文将根据课堂学习的精髓,通过详细的代码实例,带你彻底搞懂这两者的区别与联系。

一、 静态库:编译时的“整体打包”

静态库,在Linux下通常以​​.a​​(Archive)为后缀,可以理解为一个“代码包”。在编译链接阶段,链接器会从这个包里“解压”出程序需要的代码,并将其完整地复制到最终生成的可执行文件中。

1. 静态库的特点
  • 加载时机:编译时链接。一旦编译完成,可执行文件就包含了所有需要的库代码,运行时不再需要原始的库文件。
  • 内存占用:由于代码被直接复制,如果多个程序都使用了同一个静态库,那么内存中会存在多份相同的代码副本,造成资源浪费。同时,可执行文件本身的体积也会更大。
  • 更新维护:如果静态库更新了,所有使用它的程序都必须重新编译链接,才能使用新版本的库。
2. 静态库的制作与使用(代码实战)

让我们通过一个简单的数学库​​libmymath.a​​来演示,它包含加、减、乘三个函数。

目录结构:

.
├── inc
│   └── mymath.h
├── lib
├── src
│   ├── add.c
│   ├── mul.c
│   └── sub.c
└── main.c

Step 1: 准备源文件和头文件

  • ​inc/mymath.h​​ (头文件,声明函数接口)
#ifndef _MYMATH_H_
#define _MYMATH_H_

int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);

#endif
  • ​src/add.c​​, ​​src/sub.c​​, ​​src/mul.c​​ (函数实现)
// src/add.c
int add(int a, int b) {
    return a + b;
}

// src/sub.c
int sub(int a, int b) {
    return a - b;
}

// src/mul.c
int mul(int a, int b) {
    return a * b;
}

Step 2: 将源文件编译成目标文件 (.o)

我们进入​​src​​目录,使用​​gcc -c​​命令,它只编译不链接,生成​​.o​​目标文件。

$ cd src
$ gcc -c add.c sub.c mul.c
$ ls
add.c  add.o  mul.c  mul.o  sub.c  sub.o

Step 3: 使用ar​工具打包成静态库

​ar​​是GNU的归档工具,用于创建和管理静态库。

  • ​r​​:将文件插入归档(替换旧文件)。
  • ​c​​:如果归档不存在,则创建它。
  • ​s​​:创建归档的索引,这在链接时能加快速度。
# 将生成的.o文件打包成libmymath.a
$ ar rcs libmymath.a add.o sub.o mul.o

# 将生成的库文件移动到lib目录,方便管理
$ mv libmymath.a ../lib/
$ cd ..

现在,我们的​​lib​​目录下就有了​​libmymath.a​​。

Step 4: 编写主程序并链接静态库

  • ​main.c​​ (主程序)
#include <stdio.h>
#include "mymath.h" // 引用我们库的头文件

int main() {
    int a = 20;
    int b = 10;

    printf("%d + %d = %d\n", a, b, add(a, b));
    printf("%d - %d = %d\n", a, b, sub(a, b));
    printf("%d * %d = %d\n", a, b, mul(a, b));

    return 0;
}

Step 5: 编译主程序并链接

# gcc [源文件] -o [输出名] -I[头文件路径] -L[库文件路径] -l[库名]
$ gcc main.c -o static_app -I./inc -L./lib -lmymath
  • ​-I./inc​​:告诉编译器去​​./inc​​目录寻找头文件。
  • ​-L./lib​​:告诉链接器去​​./lib​​目录寻找库文件。
  • ​-lmymath​​:链接名为​​mymath​​的库。链接器会自动在前面加上​​lib​​,在后面加上​​.a​​或​​.so​​来寻找,即​​libmymath.a​​。

运行结果:

$ ./static_app
20 + 10 = 30
20 - 10 = 10
20 * 10 = 200

分析可执行文件:

# 查看文件大小
$ ls -lh static_app
-rwxr-xr-x 1 user user 8.4K Dec  8 10:00 static_app

# 使用ldd命令查看依赖,发现它不依赖我们自己的库,因为代码已经嵌入
$ ldd static_app
	linux-vdso.so.1 (0x00007ffc1b5fd000)
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1b4c000000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f1b4c418000)

可以看到,​​static_app​​不依赖于​​libmymath.a​​,因为它已经将库代码完全吸收了。


二、 动态库:运行时的“按需共享”

动态库,也叫共享库(Shared Library),在Linux下通常以​​.so​​(Shared Object)为后缀。它是一种更为现代和高效的机制。

1. 动态库的工作机制
  • 加载时机:运行时加载。编译时,链接器只在可执行文件中记录一个对库的引用,并不把代码复制进去。当程序启动时,动态链接器(​​ld-linux.so​​)会在内存中查找并加载所需的动态库。
  • 内存布局:动态库被加载到进程地址空间中的一个特殊区域,位于栈(stack)和堆(heap)之间。
  • 共享特性:这是动态库的核心优势。如果多个程序使用同一个动态库,内存中只会有一份该库的代码副本,所有进程共享它。这极大地节省了物理内存。每个进程有自己独立的数据段副本(通过写时复制实现)。
  • 延迟绑定 (Lazy Binding):为了提高启动速度,动态库采用了延迟绑定策略。程序并不会在启动时解析所有函数地址,而是在首次调用某个库函数时,才通过**PLT(Procedure Linkage Table,过程链接表)**机制去查找该函数的真实地址,并填充。后续调用则直接跳转,开销极小。
2. 动态库的优缺点
  • 优点
  • 节省内存和磁盘空间:代码共享机制效果显著。
  • 更新便捷:只需替换​​.so​​文件,无需重新编译所有使用它的程序。这对于大型软件(如游戏、操作系统组件)的更新至关重要。我们称之为“热更新”(严格来说是停止-替换-重启)。
  • 缺点
  • 调用延迟:首次函数调用有轻微的性能开销,因为需要解析地址。
  • 依赖问题:程序运行时必须能找到对应的动态库文件,否则会启动失败(即“依赖地狱”的来源之一)。
3. 动态库的制作与使用(代码实战)

我们使用与上面相同的源文件来制作动态库​​libmymath.so​​。

Step 1: 编译生成与位置无关的目标文件 (.o)

制作动态库时,必须生成与位置无关的代码(Position-Independent Code, PIC)。因为动态库会被加载到内存的任意位置,其代码不能依赖绝对地址。

# 使用 -fPIC 选项
$ cd src
$ gcc -fPIC -c add.c sub.c mul.c
$ ls
add.c  add.o  mul.c  mul.o  sub.c  sub.o

Step 2: 使用gcc -shared​打包成动态库

# -shared 选项告诉gcc生成一个共享库
$ gcc -shared -o libmymath.so add.o sub.o mul.o

# 将生成的库文件移动到lib目录
$ mv libmymath.so ../lib/
$ cd ..

现在,​​lib​​目录下就有了​​libmymath.so​​。

Step 3: 编译主程序并链接

编译命令与链接静态库时完全相同。链接器会优先选择动态库。

$ gcc main.c -o dynamic_app -I./inc -L./lib -lmymath

Step 4: 运行与问题解决

直接运行​​dynamic_app​​,大概率会遇到一个经典错误:

$ ./dynamic_app
./dynamic_app: error while loading shared libraries: libmymath.so: cannot open shared object file: No such file or directory

这是因为,虽然编译时链接器知道库在哪,但运行时,系统的动态链接器默认只在标准路径(如​​/lib​​, ​​/usr/lib​​)下查找。

解决方案(推荐开发时使用第一种):

  1. 设置LD_LIBRARY_PATH​环境变量:临时告诉动态链接器额外的搜索路径。
$ export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./lib
$ ./dynamic_app
20 + 10 = 30
20 - 10 = 10
20 * 10 = 200

运行成功!

  1. 拷贝库文件到系统目录(不推荐,会污染系统):​​sudo cp ./lib/libmymath.so /usr/lib​
  2. 修改/etc/ld.so.conf​配置文件(永久生效,需root权限)。

分析可执行文件:

# 查看文件大小,比static_app小
$ ls -lh dynamic_app
-rwxr-xr-x 1 user user 8.3K Dec  8 10:15 dynamic_app

# 使用ldd查看依赖,清晰地看到它依赖于我们制作的libmymath.so
$ ldd dynamic_app
	linux-vdso.so.1 (0x00007ffc1b5fd000)
	libmymath.so => ./lib/libmymath.so (0x00007f8b4c40e000) # 依赖关系
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8b4c000000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f8b4c818000)

三、 终极对决:动态库 vs. 静态库

为了更直观地理解,我们用一个表格来总结它们的核心区别:

特性

静态库 (​​.a​​)

动态库 (​​.so​​)

链接时机

编译时,代码被完整复制

编译时仅记录引用,运行时加载

文件体积

可执行文件较大,库本身无用

可执行文件较小,依赖库文件

内存占用

多进程多副本,占用内存高

多进程共享单副本,节省内存

更新维护

需重新编译整个程序

只需替换库文件,重启程序即可

加载速度

程序启动快,无额外加载过程

启动时需加载库,稍慢

函数调用

直接地址调用,速度快

首次调用需PLT解析,有微小延迟

部署发布

简单,单个可执行文件即可

需同时提供可执行文件和依赖的库

四、 总结与选择
  • 选择静态库的场景
  • 对执行效率要求极高,不容忍任何运行时开销的应用。
  • 环境封闭,不希望或不需要更新库的嵌入式系统。
  • 希望程序分发简单,不带一堆依赖文件。
  • 选择动态库的场景
  • 绝大多数现代应用程序,如桌面软件、服务器程序、游戏等。
  • 需要频繁更新功能模块的系统。
  • 对系统资源(特别是内存)敏感的场景。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐