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