7.7 KiB
好的,我们来系统讲一讲 动态链接库(DLL/Shared Object) 的概念、原理、基本设计原则,以及在 C/C++ 中的写法。为了全面,我会从概念到实战一步步展开。
一、什么是动态链接库(DLL/Shared Object)
-
定义
- DLL (Dynamic Link Library):Windows 系统下的动态链接库,文件后缀
.dll - SO (Shared Object):Linux/Unix 系统下的动态库,文件后缀
.so - 本质:是一段可执行代码(函数、类、资源)打包成的库,可以在程序运行时被加载,而不是编译时静态链接。
- DLL (Dynamic Link Library):Windows 系统下的动态链接库,文件后缀
-
与静态库的区别
特性 静态库 (.lib / .a) 动态库 (.dll / .so) 链接时间 编译时 运行时 可执行文件大小 大(包含库) 小(库外部) 更新 需要重新编译 只需替换库文件 共享内存 不共享 可在多个进程共享 -
工作原理
-
编译阶段:编译器使用
.h文件声明函数或类接口,不包含实现。 -
链接阶段:在 Windows 上生成
.lib导入库,在 Linux 上生成.so符号表。 -
运行阶段:
- 显式加载:程序运行时通过
LoadLibrary(Windows)或dlopen(Linux)加载。 - 隐式加载:程序启动时自动加载 DLL/so。
- 显式加载:程序运行时通过
-
二、基本设计原则
写动态库时,需要考虑以下设计原则:
-
接口与实现分离
- 提供
.h头文件定义函数/类接口 - 实现放在
.c/.cpp文件中 - 用户只依赖头文件和库文件
- 提供
-
避免全局状态
- 动态库可能被多个程序或线程使用
- 避免使用全局变量,或使用线程安全机制
-
符号导出/隐藏控制
- 只导出需要被外部调用的函数或类
- Windows:
__declspec(dllexport)/__declspec(dllimport) - Linux:
__attribute__((visibility("default")))
-
版本管理
- 避免破坏兼容性
- 增加新功能时尽量不要修改已有接口
-
异常处理
- C++ 动态库不要抛跨 DLL 的异常
- C 接口更安全(extern "C")
三、C/C++ 动态库写法
1. Windows DLL 示例(C)
my_lib.h
#ifndef MY_LIB_H
#define MY_LIB_H
#ifdef MY_LIB_BUILD_DLL
#define MY_API __declspec(dllexport)
#else
#define MY_API __declspec(dllimport)
#endif
#ifdef __cplusplus
extern "C" {
#endif
MY_API int add(int a, int b);
MY_API void hello();
#ifdef __cplusplus
}
#endif
#endif
my_lib.c
#include "my_lib.h"
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
void hello() {
printf("Hello from DLL!\n");
}
编译命令(GCC/MinGW)
gcc -DMY_LIB_BUILD_DLL -shared my_lib.c -o my_lib.dll -Wl,--out-implib,libmy_lib.a
使用 DLL 的 main.c
#include "my_lib.h"
#include <stdio.h>
int main() {
printf("%d\n", add(2, 3));
hello();
return 0;
}
2. Linux SO 示例(C)
my_lib.c 与 Windows 基本相同,只是编译方式不同:
gcc -fPIC -shared my_lib.c -o libmy_lib.so
使用 SO
#include "my_lib.h"
#include <stdio.h>
#include <dlfcn.h>
int main() {
void* handle = dlopen("./libmy_lib.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "Cannot open library: %s\n", dlerror());
return 1;
}
// 加载函数
int (*add)(int, int) = dlsym(handle, "add");
void (*hello)() = dlsym(handle, "hello");
printf("%d\n", add(2, 3));
hello();
dlclose(handle);
return 0;
}
3. C++ 类 DLL
my_class.h
#ifndef MY_CLASS_H
#define MY_CLASS_H
#ifdef MY_LIB_BUILD_DLL
#define MY_API __declspec(dllexport)
#else
#define MY_API __declspec(dllimport)
#endif
class MY_API MyClass {
public:
MyClass();
void greet();
};
#endif
my_class.cpp
#include "my_class.h"
#include <iostream>
MyClass::MyClass() {}
void MyClass::greet() {
std::cout << "Hello from C++ DLL" << std::endl;
}
注意:C++ DLL 导出类要小心跨模块内存分配和异常。
四、常用技巧与注意事项
-
显式 vs 隐式链接
- 显式:运行时加载
LoadLibrary/dlopen,灵活,便于插件化 - 隐式:编译时依赖
.lib或.so,启动自动加载
- 显式:运行时加载
-
导出 C 接口,兼容 C++
extern "C" MY_API int add(int a, int b);- 避免 C++ 名字修饰(Name Mangling)导致外部无法调用
-
线程安全
- 避免 DLL 内部静态或全局对象在多线程环境下出错
- 可以使用局部静态或线程局部存储
-
资源管理
- DLL 占用系统资源,注意释放句柄
dlclose/FreeLibrary
- DLL 占用系统资源,注意释放句柄
✅ 总结:
-
DLL/SO 是运行时可加载的库,接口/实现分离,可减少可执行文件体积并支持更新。
-
设计原则:接口分离、导出控制、避免全局状态、版本管理、异常处理。
-
写法:
- Windows:
__declspec(dllexport/dllimport)+.dll+ 可选.lib导入库 - Linux:
-fPIC -shared+.so+dlopen/dlsym/dlclose
- Windows:
-
C++:要注意名字修饰、异常、内存分配问题,必要时导出 C 接口。
如果你愿意,我可以给你画一个 DLL/so 使用流程图 + C/C++ 示例结构图,直观展示头文件、实现文件、库文件、调用端之间的关系,非常适合新手理解。
你希望我画吗?
没错!你理解得很对,.h 文件在这里就是 接口文件(Interface):
- 它 声明了函数、类或者变量,但不包含真正的实现。
- 用户只需要包含这个
.h文件就能调用库里的功能,而不需要关心实现细节。 .c/.cpp文件才是真正的 实现文件(Implementation),负责具体逻辑。
可以把它类比成 “功能说明书”:告诉你这个库能干什么、怎么调用,但具体怎么干是在背后隐藏的。
如果你愿意,我可以顺便给你画个小图,把 .h、.c/.cpp、DLL/so、调用端之间的关系可视化,一眼就懂。你想要吗?
完全正确 ✅
你理解得没错:头文件只“告诉编译器函数长啥样”,不负责引入其它库的依赖。
详细拆一下:
1️⃣ 你的 my_str.h 现在
MY_STR_API double evaluate(const char* exp);
- 只声明了
evaluate() - 没有
#include <stdio.h> - 也没有
#include <stdlib.h>等
也就是说,头文件本身没有引入 printf、malloc 等依赖
2️⃣ main.c 如果要用 printf
#include "my_str.h"
int main() {
printf("%f\n", evaluate("1+2*3"));
return 0;
}
- 编译器此时看到
printf的时候 不知道它的类型 - 会报错或者警告:
implicit declaration of printf
所以你必须:
#include <stdio.h>
#include "my_str.h"
3️⃣ 原因
my_str.h只负责提供 DLL 接口- 不负责用户程序可能会用的其它东西
- 这是 模块化的设计原则
- 否则你头文件里每一个函数都要
#include <stdio.h>,用户不想用 printf 也被迫引入,非常不优雅
4️⃣ 小结经验法则
- 头文件只放自己模块的接口和必要类型定义
- 用户程序用什么库函数,就自己 include
- DLL 内部用什么库函数,放在 DLL cpp/c 文件里 include
换句话说,头文件不会也不应该“偷带”用户的依赖” 这和 C++ 的 class 设计很像:class 内部用什么 STL,外部不用关心
你如果愿意,我可以画一张 DLL / 头文件 / 用户 main.c 的依赖关系图,让你一眼就明白谁负责什么。
你想让我画吗?