36. 【C语言】程序的动态加载:静态库与动态库的制作与使用

前面三十五篇文章,我们写了很多函数模块——排序、链表、文件工具。但每次在新项目里想用它们,都得把 .c 文件复制来复制去,或者手动添加到编译命令里。当代码积累到几十个模块、被多个项目共用时,这种方式就太原始了。

成熟的软件工程会把常用的代码打包成库(Library)。库就是预先编译好的目标代码集合,你的程序可以直接链接它,不用每次都重新编译那些源文件。C 语言的库分为两种:静态库.a / .lib)和动态库.so / .dylib / .dll)。今天我们就来学会怎么制作它们、使用它们,以及动态库独有的运行时加载技术。


一、库是什么?为什么需要它?

库的本质就是可复用的编译后代码包。使用库的好处:

  1. 不用重复编译:常用的功能(数学计算、加密、压缩)预先编译好,链接时直接拿来用。
  2. 隐藏实现:你可以只提供库文件和头文件,别人看不到源码。
  3. 便于分发和更新:特别是动态库,可以独立更新而不需要重新编译整个程序。

C 标准库本身就是最典型的例子——你每天都在用 libc.a(静态)或 libc.so(动态),却从没编译过 printf 的源码。


二、静态库:.a 文件(Linux/macOS)或 .lib(Windows)

静态库在链接阶段会被完整复制到最终的可执行文件中。程序运行时不再需要库文件。

1. 制作静态库

假设我们有两个工具函数模块:

math_utils.h

#ifndef MATH_UTILS_H
#define MATH_UTILS_H

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

#endif

math_utils.c

#include "math_utils.h"

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

string_utils.h

#ifndef STRING_UTILS_H
#define STRING_UTILS_H

int str_length(const char *s);
void str_toupper(char *s);

#endif

string_utils.c

#include "string_utils.h"
#include <ctype.h>

int str_length(const char *s) {
    int len = 0;
    while (*s++) len++;
    return len;
}

void str_toupper(char *s) {
    while (*s) {
        *s = toupper((unsigned char)*s);
        s++;
    }
}

步骤一:把每个 .c 编译成目标文件 .o

gcc -c math_utils.c -o math_utils.o
gcc -c string_utils.c -o string_utils.o

-c 表示只编译、汇编,不链接。

步骤二:用 ar 工具打包成静态库

ar rcs libutils.a math_utils.o string_utils.o
  • r:插入目标文件到归档(若已存在则替换)。
  • c:创建归档文件(如果不存在)。
  • s:生成索引,供链接器快速查找符号。

静态库的命名惯例是 lib 开头,后缀 .a(archive)。库名叫 utils

Windows:

rcs libutils.lib math_utils.o string_utils.o

2. 使用静态库

写一个主程序:

main.c

#include <stdio.h>
#include "math_utils.h"
#include "string_utils.h"

int main(void) {
    printf("10 + 5 = %d\n", add(10, 5));
    printf("6 * 7 = %d\n", multiply(6, 7));

    char text[] = "hello, world";
    printf("长度: %d\n", str_length(text));
    str_toupper(text);
    printf("大写: %s\n", text);

    return 0;
}

编译并链接静态库:

gcc main.c -L. -lutils -o app
  • -L.:告诉链接器在当前目录搜索库文件。
  • -lutils:链接名为 libutils.a 的库(lib.a 省略,只写中间的名字)。

也可以直接把库文件全名当普通目标文件传入:

gcc main.c libutils.a -o app

运行:

./app

app 是一个完全独立的可执行文件,不需要 libutils.a 就能运行。因为它的代码已经被完整复制进去了。

Windows环境:
# 编译成目标文件
gcc -c math_utils.c -o math_utils.o
gcc -c string_utils.c -o string_utils.o

# 用 ar 工具打包成静态库
ar rcs libutils.lib math_utils.o string_utils.o

# 编译并链接静态库
gcc main.c libutils.lib -o app.exe

# 运行
./app.exe


三、动态库:.so(Linux)、.dylib(macOS)、.dll(Windows)

动态库在运行时才加载,可执行文件里只记录“我需要哪个库的哪个符号”。优点:

  • 节省磁盘和内存:多个程序可以共享同一个动态库,内存中只加载一份。
  • 更新方便:修了库的 bug,替换 .so 文件就行,不需要重新编译程序(只要接口不变)。
  • 插件机制:可以在程序运行时按需加载库。

代价:程序启动时需要动态链接器解析符号,有微小开销;库文件丢失则程序无法运行。

1. 制作动态库

用同样的源文件:

gcc -c -fPIC math_utils.c -o math_utils.o
gcc -c -fPIC string_utils.c -o string_utils.o

-fPIC(Position Independent Code,位置无关代码)是必须的。动态库会被加载到内存的任意地址,代码必须能够在该地址正确运行,不能依赖绝对地址。

然后把 .o 链接成动态库:

gcc -shared -o libutils.so math_utils.o string_utils.o
  • -shared:生成共享库。
  • 输出文件名 libutils.so(Linux)。macOS 用 .dylib,Windows 用 .dll(编译命令不同,见文末附录)。

也可以一步到位:

gcc -shared -fPIC math_utils.c string_utils.c -o libutils.so

2. 使用动态库

gcc main.c -L. -lutils -o app

链接命令和静态库完全一样。链接器发现 libutils.so 存在时,会优先使用动态库(除非用 -static 强制静态)。

但运行时,系统需要能找到 libutils.so。默认只搜索系统目录(/lib/usr/lib 等)。临时解决方案:

LD_LIBRARY_PATH=. ./app   # Linux
DYLD_LIBRARY_PATH=. ./app # macOS

永久方案是将库安装到标准路径,或在编译时把搜索路径写进可执行文件:

gcc main.c -L. -lutils -Wl,-rpath,'$ORIGIN' -o app

-Wl,-rpath,'$ORIGIN' 告诉链接器:在可执行文件里写入一个额外的动态库搜索路径——$ORIGIN 指可执行文件所在目录。这样只要 .soapp 在同一文件夹,就能找到。

3. 验证依赖

ldd(Linux)或 otool -L(macOS)可以查看可执行文件依赖的动态库:

ldd ./app
# 输出中会看到 libutils.so => ...

四、动态加载:在程序运行时才加载库

上面的动态库是程序启动时自动加载的。还有一种更灵活的方式:在程序运行到一半时手动加载库,获取函数地址,调用它。这就是动态加载(Dynamic Loading),是实现插件系统的基础。

需要 #include <dlfcn.h>,并在链接时加 -ldl

核心函数

void *dlopen(const char *filename, int flag);  // 打开动态库,返回句柄
void *dlsym(void *handle, const char *symbol);  // 查找符号(函数或变量)地址
int   dlclose(void *handle);                    // 关闭句柄
char *dlerror(void);                           // 返回最近的错误描述

示例:运行时加载 libutils.so 并调用函数

#include <stdio.h>
#include <dlfcn.h>

int main(void) {
    // 1. 打开动态库
    void *handle = dlopen("./libutils.so", RTLD_LAZY);
    if (!handle) {
        fprintf(stderr, "dlopen 失败: %s\n", dlerror());
        return 1;
    }

    // 2. 查找函数符号
    int (*add_func)(int, int) = (int (*)(int, int))dlsym(handle, "add");
    if (!add_func) {
        fprintf(stderr, "找不到 add 函数: %s\n", dlerror());
        dlclose(handle);
        return 1;
    }

    // 3. 调用
    printf("通过动态加载调用 add(10, 20) = %d\n", add_func(10, 20));

    // 4. 关闭句柄
    dlclose(handle);
    return 0;
}

编译这个程序需要链接 libdl

gcc dynamic_main.c -ldl -o dynamic_app
./dynamic_app

dlsym 返回的是 void*,必须强制转换成与函数签名匹配的函数指针。这就是函数指针(第十八篇)的最强应用场景之一——连函数本身都是在运行时才确定的。

何时使用动态加载?

  • 插件系统:程序启动时扫描 plugins/ 目录下的 .so 文件,加载并注册。
  • 可选功能:某些功能依赖大型库,用户不一定需要,按需加载减少启动开销。
  • 热更新:长期运行的服务,不重启替换某个模块的实现。

五、静态库 vs 动态库:选择指南

静态库动态库
链接方式编译时复制进可执行文件运行时加载
文件体积可执行文件变大可执行文件小
内存占用每个程序独立一份多程序共享一份
更新需重新编译程序替换库文件即可
部署简单(单个文件)需附带库或确保系统有
启动速度稍快(无动态解析)稍慢(需加载和符号解析)
适用场景嵌入式、单文件分发桌面应用、服务端、插件

对于个人练习和小工具,静态链接省事。对于大型系统、需要支持插件机制、或者库会被多个程序共享的场景,动态库是更好的选择。


六、附录:各平台命令对照

操作LinuxmacOSWindows (MinGW)
编译目标文件gcc -c file.c同左同左
创建静态库ar rcs libx.a file.o同左ar rcs libx.lib file.o
创建动态库gcc -shared -fPIC -o libx.so file.cgcc -shared -fPIC -o libx.dylib file.cgcc -shared -o x.dll file.c -Wl,--out-implib,libx.a
查看依赖ldd ./appotool -L ./app`objdump -p app.exe
运行时库搜索路径LD_LIBRARY_PATHDYLD_LIBRARY_PATHPATH(或放在 exe 同目录)

七、小结

今天你掌握了如何把代码打包成库,以及静态库和动态库的完整生命周期:

  • 静态库(.aar rcs 制作,链接时完整复制,独立运行,适合小工具和嵌入式。
  • 动态库(.so/.dylib/.dllgcc -shared -fPIC 制作,运行时加载,节省内存,支持插件和独立更新。
  • 动态加载(dlopen/dlsym/dlclose:在程序运行时按需加载库,是插件系统的基石。
  • 动态库的搜索路径可以通过 LD_LIBRARY_PATH-rpath 控制。

学会库的使用,你的 C 语言项目就可以从“一堆源文件”进化到“有组织的软件架构”。这也是理解大型 C/C++ 项目(如 Python 解释器、数据库引擎、游戏引擎)的编译和运行机制的关键基础。

至此,系统编程的“文件与存储”部分基本覆盖完毕——从标准 I/O 到系统调用,从文本到二进制,从静态链接到动态加载。下一篇,我们要进入一个全新的领域:并发编程。学会创建多线程,用互斥锁保护共享数据,让你的程序真正“一心多用”。


课后小练习

  1. 将之前实现的链表模块打包为静态库 liblinkedlist.a,编写一个主程序链接并使用它。
  2. 将同一链表模块编译为动态库 liblinkedlist.so,用同一个主程序链接动态库运行。用 ldd(或对应工具)查看程序依赖。
  3. 写一个程序,使用 dlopendlsym 动态加载 libm.so(数学库),在运行时获取 sqrt 函数的地址并调用它计算平方根。
  4. (小挑战)设计一个简单的插件接口:规定每个插件必须实现 void plugin_init(void)void plugin_run(void)。创建两个“插件”动态库(plugin_a.soplugin_b.so),主程序在运行时依次加载 plugins/ 目录下的所有 .so 文件并执行。体验插件系统的设计思路。

我们下期见!

💡获取本系列示例代码请访问 GitCode 仓库

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值