第14章 静态库与动态库

1. 库的概念

1.1 什么是库

库是一组预编译的函数和数据的集合,用于被其他程序调用。库的主要作用是代码重用和模块化,将常用功能封装成库,可以被多个程序共享使用。

1.2 库的类型

C语言中主要有两种类型的库:

  • 静态库:在编译时将库代码复制到可执行文件中
  • 动态库:在运行时加载到内存中,被多个程序共享使用

1.3 库的优缺点

静态库的优点

  • 可执行文件不依赖外部库,可独立运行
  • 加载速度快,因为代码已经包含在可执行文件中
  • 编译时可以进行优化,提高性能

静态库的缺点

  • 可执行文件体积大,因为包含了库代码
  • 库更新后需要重新编译所有使用该库的程序
  • 多个程序使用同一个库时,会在内存中存在多份副本

动态库的优点

  • 可执行文件体积小,因为不包含库代码
  • 库更新后不需要重新编译使用该库的程序
  • 多个程序使用同一个库时,在内存中只存在一份副本
  • 可以在运行时动态加载和卸载

动态库的缺点

  • 可执行文件依赖外部库,需要确保库存在
  • 加载速度比静态库慢
  • 编译时的优化空间较小

2. 静态库的创建与使用

2.1 静态库的创建

2.1.1 编译源文件

首先,将源文件编译成目标文件:

1
2
gcc -c utils.c -o utils.o
gcc -c math.c -o math.o

2.1.2 创建静态库

使用ar命令创建静态库:

1
ar rcs libutils.a utils.o math.o

其中:

  • r:替换或添加文件到库中
  • c:创建库(如果不存在)
  • s:生成索引,加速链接过程

2.2 静态库的使用

2.2.1 编译时链接静态库

1
gcc main.c -L. -lutils -o program

其中:

  • -L.:指定库文件搜索路径为当前目录
  • -lutils:链接名为libutils.a的静态库

2.2.2 直接指定静态库文件

1
gcc main.c libutils.a -o program

2.3 静态库的管理

2.3.1 查看静态库内容

使用ar命令查看静态库包含的目标文件:

1
ar t libutils.a

2.3.2 提取静态库中的文件

使用ar命令提取静态库中的目标文件:

1
ar x libutils.a utils.o

2.3.3 更新静态库

使用ar命令更新静态库中的文件:

1
ar r libutils.a new_utils.o

3. 动态库的创建与使用

3.1 动态库的创建

3.1.1 编译源文件

使用-fPIC选项编译源文件,生成位置无关代码:

1
2
gcc -fPIC -c utils.c -o utils.o
gcc -fPIC -c math.c -o math.o

3.1.2 创建动态库

使用-shared选项创建动态库:

Linux

1
gcc -shared -o libutils.so utils.o math.o

Windows

1
gcc -shared -o utils.dll utils.o math.o

macOS

1
gcc -shared -o libutils.dylib utils.o math.o

3.2 动态库的使用

3.2.1 编译时链接动态库

1
gcc main.c -L. -lutils -o program

3.2.2 运行时加载动态库

在Linux系统中,需要确保动态库能够被找到:

  1. 设置LD_LIBRARY_PATH环境变量
1
2
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./program
  1. 将库文件复制到系统库目录
1
2
sudo cp libutils.so /usr/lib/
./program
  1. 更新动态库缓存
1
sudo ldconfig

3.3 动态库的管理

3.3.1 查看动态库依赖

使用ldd命令查看可执行文件依赖的动态库:

1
ldd program

3.3.2 查看动态库内容

使用nm命令查看动态库中的符号:

1
nm -D libutils.so

3.3.3 动态库版本管理

动态库通常使用版本号进行管理,如libutils.so.1.0.0。版本号由三部分组成:

  • 主版本号:不兼容的API变更
  • 次版本号:向后兼容的API添加
  • 修订版本号:向后兼容的错误修复

4. 动态加载库

4.1 什么是动态加载库

动态加载库是在程序运行时使用dlopen()等函数加载的库,而不是在编译时链接的库。动态加载库的主要优点是可以在运行时根据需要加载和卸载库,提高程序的灵活性。

4.2 动态加载库的函数

4.2.1 dlopen()

1
2
3
#include <dlfcn.h>

void *dlopen(const char *filename, int flags);

参数

  • filename:库文件路径
  • flags:加载标志,如RTLD_LAZY(延迟绑定)或RTLD_NOW(立即绑定)

返回值

  • 成功:返回库的句柄
  • 失败:返回NULL,并设置dlerror()

4.2.2 dlsym()

1
void *dlsym(void *handle, const char *symbol);

参数

  • handle:库的句柄
  • symbol:要查找的符号名

返回值

  • 成功:返回符号的地址
  • 失败:返回NULL,并设置dlerror()

4.2.3 dlclose()

1
int dlclose(void *handle);

参数

  • handle:库的句柄

返回值

  • 成功:返回0
  • 失败:返回非0,并设置dlerror()

4.2.4 dlerror()

1
char *dlerror(void);

返回值

  • 成功:返回错误信息字符串
  • 失败:返回NULL

4.3 动态加载库的示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <stdio.h>
#include <dlfcn.h>

int main() {
void *handle;
int (*add)(int, int);
char *error;

// 加载动态库
handle = dlopen("./libutils.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}

// 查找add函数
add = (int (*)(int, int)) dlsym(handle, "add");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
dlclose(handle);
return 1;
}

// 调用add函数
printf("1 + 2 = %d\n", add(1, 2));

// 关闭动态库
dlclose(handle);

return 0;
}

编译命令

1
gcc main.c -ldl -o program

5. 库的设计

5.1 库的接口设计

5.1.1 接口原则

  • 最小化接口:只暴露必要的函数和数据
  • 稳定性:接口一旦发布,应保持稳定
  • 一致性:接口设计应遵循一致的命名和参数风格
  • 文档化:为接口提供详细的文档

5.1.2 接口声明

在头文件中声明库的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// utils.h
#ifndef UTILS_H
#define UTILS_H

#ifdef __cplusplus
extern "C" {
#endif

int add(int a, int b);
int subtract(int a, int b);

#ifdef __cplusplus
}
#endif

#endif // UTILS_H

5.2 库的内部实现

5.2.1 内部函数和变量

使用static修饰符定义仅在库内部使用的函数和变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// utils.c
#include "utils.h"

// 内部变量
static int s_counter = 0;

// 内部函数
static void internal_function() {
// 实现
}

// 公共函数
int add(int a, int b) {
s_counter++;
return a + b;
}

int subtract(int a, int b) {
s_counter++;
return a - b;
}

5.2.2 库的初始化和清理

为库提供初始化和清理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// utils.h
void utils_init();
void utils_cleanup();

// utils.c
static bool s_initialized = false;

void utils_init() {
if (!s_initialized) {
// 初始化代码
s_initialized = true;
}
}

void utils_cleanup() {
if (s_initialized) {
// 清理代码
s_initialized = false;
}
}

5.3 库的错误处理

5.3.1 错误码

使用错误码表示库函数的执行状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// utils.h
typedef enum {
UTILS_SUCCESS = 0,
UTILS_ERROR_INVALID_PARAM = 1,
UTILS_ERROR_OUT_OF_MEMORY = 2,
UTILS_ERROR_UNKNOWN = 3
} utils_error_t;

utils_error_t utils_divide(int a, int b, int *result);

// utils.c
utils_error_t utils_divide(int a, int b, int *result) {
if (!result) {
return UTILS_ERROR_INVALID_PARAM;
}

if (b == 0) {
return UTILS_ERROR_INVALID_PARAM;
}

*result = a / b;
return UTILS_SUCCESS;
}

5.3.2 错误消息

提供获取错误消息的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// utils.h
const char *utils_strerror(utils_error_t error);

// utils.c
const char *utils_strerror(utils_error_t error) {
switch (error) {
case UTILS_SUCCESS:
return "Success";
case UTILS_ERROR_INVALID_PARAM:
return "Invalid parameter";
case UTILS_ERROR_OUT_OF_MEMORY:
return "Out of memory";
case UTILS_ERROR_UNKNOWN:
return "Unknown error";
default:
return "Invalid error code";
}
}

6. 库的构建系统

6.1 使用Makefile构建库

构建静态库的Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CC = gcc
CFLAGS = -Wall -Wextra -I.
AR = ar
ARFLAGS = rcs

SRC = utils.c math.c
OBJ = $(SRC:.c=.o)
TARGET = libutils.a

all: $(TARGET)

$(TARGET): $(OBJ)
$(AR) $(ARFLAGS) $@ $^

%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<

clean:
rm -f $(OBJ) $(TARGET)

.PHONY: all clean

构建动态库的Makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CC = gcc
CFLAGS = -Wall -Wextra -I. -fPIC
LDFLAGS = -shared

SRC = utils.c math.c
OBJ = $(SRC:.c=.o)
TARGET = libutils.so

all: $(TARGET)

$(TARGET): $(OBJ)
$(CC) $(LDFLAGS) -o $@ $^

%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<

clean:
rm -f $(OBJ) $(TARGET)

.PHONY: all clean

6.2 使用CMake构建库

CMakeLists.txt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmake_minimum_required(VERSION 3.10)
project(utils VERSION 1.0.0)

set(CMAKE_C_STANDARD 99)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_library(utils STATIC
utils.c
math.c
)

# 或者构建动态库
# add_library(utils SHARED
# utils.c
# math.c
# )

target_include_directories(utils PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)

7. 库的测试

7.1 单元测试

为库编写单元测试,确保库的功能正确:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// test_utils.c
#include "utils.h"
#include <stdio.h>

int test_add() {
int result = add(1, 2);
if (result != 3) {
printf("Test add failed: expected 3, got %d\n", result);
return 1;
}
printf("Test add passed\n");
return 0;
}

int test_subtract() {
int result = subtract(5, 3);
if (result != 2) {
printf("Test subtract failed: expected 2, got %d\n", result);
return 1;
}
printf("Test subtract passed\n");
return 0;
}

int main() {
int failures = 0;
failures += test_add();
failures += test_subtract();

if (failures == 0) {
printf("All tests passed\n");
return 0;
} else {
printf("%d tests failed\n", failures);
return 1;
}
}

编译命令

1
2
gcc test_utils.c -L. -lutils -o test_utils
./test_utils

7.2 性能测试

为库编写性能测试,确保库的性能满足要求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// perf_test.c
#include "utils.h"
#include <stdio.h>
#include <time.h>

void test_performance() {
clock_t start, end;
double cpu_time_used;
int i, result;

start = clock();

// 执行大量计算
for (i = 0; i < 10000000; i++) {
result = add(i, i + 1);
}

end = clock();
cpu_time_used = ((double) (end - start)) / CLOCKS_PER_SEC;

printf("Performance test: %f seconds\n", cpu_time_used);
}

int main() {
test_performance();
return 0;
}

8. 库的部署

8.1 静态库的部署

静态库的部署比较简单,只需要将库文件和头文件提供给用户即可:

  1. 提供头文件:如utils.h
  2. 提供静态库文件:如libutils.a
  3. 提供使用示例:如example.c
  4. 提供构建说明:如README.md

8.2 动态库的部署

动态库的部署需要考虑运行时库的查找问题:

  1. 提供头文件:如utils.h
  2. 提供动态库文件:如libutils.so
  3. 提供使用示例:如example.c
  4. 提供构建说明:如README.md
  5. 提供安装脚本:将库文件复制到系统库目录

8.3 库的版本管理

库的版本管理是确保库的兼容性和稳定性的重要手段:

  1. 使用语义化版本号:如1.0.0
  2. 保持向后兼容:尽量不要修改现有API的行为
  3. 提供版本信息:在库中提供获取版本信息的函数
  4. 使用符号版本:在动态库中使用符号版本控制

9. 库的最佳实践

9.1 命名规范

  • 库名:使用小写字母和下划线,如libutils
  • 头文件名:使用小写字母和下划线,如utils.h
  • 函数名:使用小写字母和下划线,如utils_add
  • 常量名:使用大写字母和下划线,如UTILS_SUCCESS
  • 类型名:使用_t后缀,如utils_error_t

9.2 代码规范

  • 使用一致的代码风格:如缩进、命名、注释等
  • 添加详细的注释:特别是公共接口
  • 使用头文件保护符:防止头文件被重复包含
  • 使用extern "C":确保C++代码可以调用C库
  • 避免使用全局变量:如果必须使用,使用静态全局变量

9.3 性能优化

  • 减少函数调用开销:对于频繁调用的函数,考虑内联
  • 减少内存分配:使用静态缓冲区或对象池
  • 优化算法:选择高效的算法和数据结构
  • 使用编译器优化:如-O2-O3等优化选项
  • 避免不必要的计算:缓存计算结果

9.4 安全性

  • 检查参数:验证所有函数参数的有效性
  • 防止缓冲区溢出:使用安全的字符串处理函数
  • 释放资源:确保所有分配的资源都被释放
  • 避免使用不安全的函数:如getsstrcpy
  • 使用地址随机化:编译时启用地址随机化

10. 常见问题与解决方案

10.1 静态库问题

10.1.1 链接错误:未定义的引用

原因:静态库中缺少某些函数的定义
解决:确保所有声明的函数都有定义,使用nm命令检查库中的符号

10.1.2 静态库重复定义

原因:多个静态库包含相同的函数定义
解决:使用ar命令提取需要的目标文件,重新创建库

10.2 动态库问题

10.2.1 运行时错误:找不到动态库

原因:动态库路径未包含在LD_LIBRARY_PATH
解决:设置LD_LIBRARY_PATH环境变量,或将库文件复制到系统库目录

10.2.2 动态库版本冲突

原因:系统中存在多个版本的动态库
解决:使用版本号管理动态库,确保使用正确版本的库

10.2.3 动态库符号冲突

原因:多个动态库导出相同名称的符号
解决:使用命名空间(通过命名前缀),或使用静态链接

10.3 跨平台问题

10.3.1 不同平台的库格式

原因:不同平台使用不同的库格式
解决:为每个平台编译对应的库格式

10.3.2 不同平台的API差异

原因:不同平台的系统API存在差异
解决:使用条件编译,为不同平台提供不同的实现

11. 库的工具

11.1 编译工具

  • gcc:GNU编译器集合
  • clang:LLVM编译器
  • msvc:Microsoft Visual C++编译器

11.2 库工具

  • ar:创建和管理静态库
  • ld:链接器
  • nm:查看符号表
  • objdump:查看目标文件信息
  • readelf:查看ELF文件信息
  • strip:移除符号表,减小文件体积

11.3 调试工具

  • gdb:调试器
  • valgrind:内存分析工具
  • ltrace:跟踪库函数调用
  • strace:跟踪系统调用

12. 示例代码

12.1 静态库示例

utils.h

1
2
3
4
5
6
7
8
9
#ifndef UTILS_H
#define UTILS_H

int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
int divide(int a, int b);

#endif // UTILS_H

utils.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include "utils.h"

int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}

int multiply(int a, int b) {
return a * b;
}

int divide(int a, int b) {
if (b == 0) {
return 0;
}
return a / b;
}

创建静态库

1
2
gcc -c utils.c -o utils.o
ar rcs libutils.a utils.o

使用静态库

main.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include "utils.h"

int main() {
int a = 10, b = 5;
printf("%d + %d = %d\n", a, b, add(a, b));
printf("%d - %d = %d\n", a, b, subtract(a, b));
printf("%d * %d = %d\n", a, b, multiply(a, b));
printf("%d / %d = %d\n", a, b, divide(a, b));
return 0;
}

编译命令

1
2
gcc main.c -L. -lutils -o program
./program

12.2 动态库示例

创建动态库

1
2
gcc -fPIC -c utils.c -o utils.o
gcc -shared -o libutils.so utils.o

使用动态库

main.c

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>
#include "utils.h"

int main() {
int a = 10, b = 5;
printf("%d + %d = %d\n", a, b, add(a, b));
printf("%d - %d = %d\n", a, b, subtract(a, b));
printf("%d * %d = %d\n", a, b, multiply(a, b));
printf("%d / %d = %d\n", a, b, divide(a, b));
return 0;
}

编译命令

1
2
3
gcc main.c -L. -lutils -o program
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./program

12.3 动态加载库示例

main.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <stdio.h>
#include <dlfcn.h>

int main() {
void *handle;
int (*add)(int, int);
int (*subtract)(int, int);
char *error;

// 加载动态库
handle = dlopen("./libutils.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s\n", dlerror());
return 1;
}

// 查找add函数
add = (int (*)(int, int)) dlsym(handle, "add");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
dlclose(handle);
return 1;
}

// 查找subtract函数
subtract = (int (*)(int, int)) dlsym(handle, "subtract");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s\n", error);
dlclose(handle);
return 1;
}

// 调用函数
printf("10 + 5 = %d\n", add(10, 5));
printf("10 - 5 = %d\n", subtract(10, 5));

// 关闭动态库
dlclose(handle);

return 0;
}

编译命令

1
2
3
gcc main.c -ldl -o program
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:.
./program