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

好的,我们来系统讲一讲 动态链接库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 符号表。

    • 运行阶段

      • 显式加载:程序运行时通过 LoadLibraryWindowsdlopenLinux加载。
      • 隐式加载:程序启动时自动加载 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

#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 导出类要小心跨模块内存分配和异常。


四、常用技巧与注意事项

  1. 显式 vs 隐式链接

    • 显式:运行时加载 LoadLibrary/dlopen,灵活,便于插件化
    • 隐式:编译时依赖 .lib.so,启动自动加载
  2. 导出 C 接口,兼容 C++

    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 现在

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 小结经验法则

  1. 头文件只放自己模块的接口和必要类型定义
  2. 用户程序用什么库函数,就自己 include
  3. DLL 内部用什么库函数,放在 DLL cpp/c 文件里 include

换句话说,头文件不会也不应该“偷带”用户的依赖” 这和 C++ 的 class 设计很像class 内部用什么 STL外部不用关心


你如果愿意,我可以画一张 DLL / 头文件 / 用户 main.c 的依赖关系图,让你一眼就明白谁负责什么。

你想让我画吗?