6、动态库与静态库:Windows (DLL, LIB) 与 Linux/macOS (so, a, dylib) 的创建与调用
说到库,我估计不少朋友都栽过跟头。明明代码写得没问题,一链接就报“未解析的外部符号”。或者换个平台编译,库文件格式不对,直接链接失败。嗯,今天咱们就把这层窗户纸捅破。
说白了,库就是一堆已经编译好的目标文件(.o 或 .obj)的集合。它存在的意义就两个:代码复用和隐藏实现。你给别人一个库,别人能用你的函数,但看不到源码。这在商业软件里太常见了。
6.1 静态库 vs 动态库:到底选哪个?
我刚开始做项目时,总觉得静态库省事,链接进去就完事了。后来项目大了才发现,每个可执行文件都带着一份同样的代码,磁盘和内存都吃不消。你想想看,如果十个程序都用同一个日志库,静态链接的话,内存里就有十份日志库的代码。动态库就只加载一份,大家共享。
| 特性 | 静态库 | 动态库 |
|---|---|---|
| 文件后缀(Windows) | .lib | .dll(附带 .lib 导入库) |
| 文件后缀(Linux) | .a | .so |
| 文件后缀(macOS) | .a | .dylib |
| 链接时机 | 编译链接时 | 程序加载时或运行时 |
| 可执行文件大小 | 大(包含库代码) | 小(仅引用) |
| 更新部署 | 需重新链接整个程序 | 替换 .dll/.so 即可 |
| 依赖管理 | 无运行时依赖 | 需确保目标系统有对应库 |
我的建议:工具类、基础算法库这种几乎不变的东西,用静态库省心。插件、UI组件、需要频繁更新的模块,用动态库。我在做嵌入式交叉编译时,就吃过动态库依赖的亏——目标板上缺一个 .so,程序直接崩了。从那以后,嵌入式项目我基本只用静态库。
6.2 静态库的创建与调用
静态库的创建其实很简单。就是把 .c 文件编译成 .o 文件,然后用归档工具打包。咱们先看一个最简单的例子。
6.2.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;
}
6.2.2 Windows 下创建静态库
Windows 下用 Visual Studio 的命令行工具。我个人习惯用 Developer Command Prompt。
cl /c math_utils.c // 编译成 math_utils.obj
lib /OUT:math_utils.lib math_utils.obj // 打包成静态库
调用时,在代码里包含头文件,链接时加上 .lib 文件:
cl main.c math_utils.lib
6.2.3 Linux/macOS 下创建静态库
gcc -c math_utils.c -o math_utils.o
ar rcs libmath_utils.a math_utils.o
调用:
gcc main.c -L. -lmath_utils -o main
注意:Linux 下静态库命名必须遵循 lib[name].a 的格式。链接时用 -l[name] 去掉 lib 和 .a。这是约定俗成的规矩,别搞错了。
6.3 动态库的创建与调用
动态库就稍微复杂一点了。Windows 和 Linux 的差异比较大,尤其是导出符号的处理。我曾经在 Windows 下写了一个 DLL,导出函数名被编译器“修饰”了,结果调用方死活找不到函数。嗯,这里要特别注意。
6.3.1 Windows DLL 的创建
Windows 下需要用到 __declspec(dllexport) 和 __declspec(dllimport)。我一般用宏来统一处理:
// math_utils.h
#ifdef MATH_UTILS_EXPORTS
#define MATH_API __declspec(dllexport)
#else
#define MATH_API __declspec(dllimport)
#endif
MATH_API int add(int a, int b);
MATH_API int multiply(int a, int b);
// math_utils.c
#define MATH_UTILS_EXPORTS
#include "math_utils.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
编译 DLL:
cl /LD math_utils.c /Fe:math_utils.dll
这会生成 math_utils.dll 和 math_utils.lib(导入库)。调用方链接时用 .lib,运行时需要 .dll。
6.3.2 Linux .so 的创建
Linux 下就简单多了,不需要导出声明。默认所有非 static 函数都是导出的。
gcc -fPIC -c math_utils.c -o math_utils.o
gcc -shared -o libmath_utils.so math_utils.o
-fPIC 是位置无关代码,动态库必须加这个选项。调用:
gcc main.c -L. -lmath_utils -o main
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./main
避坑指南:我曾经在 Linux 下编译动态库忘了加 -fPIC,链接时报了“relocation R_X86_64_PC32 against symbol”错误。折腾了半天才发现是这个问题。记住:64位系统下动态库必须用 -fPIC 编译。
6.3.3 macOS .dylib 的创建
macOS 和 Linux 类似,但有个小区别:
gcc -fPIC -c math_utils.c -o math_utils.o
gcc -dynamiclib -o libmath_utils.dylib math_utils.o
调用:
gcc main.c -L. -lmath_utils -o main
export DYLD_LIBRARY_PATH=.:$DYLD_LIBRARY_PATH
./main
6.4 运行时动态加载
有时候你不想在程序启动时就加载动态库,而是按需加载。比如插件系统,用户装了什么插件就加载什么。这时候就需要运行时加载。
| 功能 | Windows | Linux/macOS |
|---|---|---|
| 加载库 | LoadLibrary | dlopen |
| 获取函数地址 | GetProcAddress | dlsym |
| 卸载库 | FreeLibrary | dlclose |
| 错误信息 | GetLastError | dlerror |
看一个跨平台的例子:
#ifdef _WIN32
#include <windows.h>
#define LIB_HANDLE HMODULE
#define LOAD_LIB(name) LoadLibrary(name)
#define GET_FUNC(handle, name) GetProcAddress(handle, name)
#define CLOSE_LIB(handle) FreeLibrary(handle)
#else
#include <dlfcn.h>
#define LIB_HANDLE void*
#define LOAD_LIB(name) dlopen(name, RTLD_LAZY)
#define GET_FUNC(handle, name) dlsym(handle, name)
#define CLOSE_LIB(handle) dlclose(handle)
#endif
int main() {
LIB_HANDLE handle = LOAD_LIB("math_utils.dll"); // 或 .so / .dylib
if (!handle) {
// 错误处理
return -1;
}
typedef int (*func_t)(int, int);
func_t add_func = (func_t)GET_FUNC(handle, "add");
if (add_func) {
int result = add_func(3, 4);
printf("3 + 4 = %d\n", result);
}
CLOSE_LIB(handle);
return 0;
}
个人经验:运行时加载时,函数签名一定要匹配。我见过有人把返回 int 的函数当成返回 void* 来调用,结果栈乱了,程序莫名其妙崩溃。建议用 typedef 定义函数指针类型,清晰明了。
6.5 知识体系总览
下面这张图把本章的核心逻辑串起来了。静态库和动态库的创建流程、跨平台差异、调用方式,一目了然。
6.6 跨平台移植要点
最后总结几个我踩过的坑,希望能帮你少走弯路:
- 符号可见性:Windows 默认不导出任何符号,必须用 __declspec(dllexport)。Linux 默认全部导出,但可以用 __attribute__((visibility("hidden"))) 控制。如果你写跨平台库,建议统一用宏控制导出。
- 名字修饰:Windows 下 C 函数用 __stdcall 调用约定时,函数名会被修饰(比如 _add@8)。用 .def 文件或 extern "C" 可以避免。C++ 更复杂,有名字粉碎(name mangling)问题。
- 运行时依赖:动态库的搜索路径,Windows 是当前目录和 PATH,Linux 是 LD_LIBRARY_PATH,macOS 是 DYLD_LIBRARY_PATH。部署时一定要确认库文件在正确的位置。
- ABI 兼容性:不同编译器、甚至同一编译器的不同版本,生成的库可能不兼容。结构体对齐、函数调用约定、异常处理机制都可能不同。我建议库的发布方提供多版本编译好的二进制文件。
一句话总结:静态库省心但费空间,动态库灵活但需管理依赖。跨平台开发时,用宏封装平台差异,用条件编译处理不同 API。记住这些,库的创建和调用就不再是难题了。