Files
Data-Structure/DynamicLinkLibrary/readme.md
2025-12-28 16:11:38 +08:00

327 lines
7.7 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

好的,我们来系统讲一讲 **动态链接库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 的依赖关系图**,让你一眼就明白谁负责什么。
你想让我画吗?