单例对象必须驻留在DLL内部的.data节中,最根本前提是实例不能在调用方构造或通过std::shared_ptr跨模块传递,否则导致控制块分裂、析构错堆和访问冲突;正确做法是声明为DLL内static全局变量,并用__declspec(dllexport)或__attribute__((visibility("default")))导出其地址。

单例对象必须驻留在DLL内部的.data节中
跨DLL共享单例,最根本的前提是:实例本身不能在调用方(如EXE)里构造,也不能靠std::shared_ptr在多个模块间传递。否则控制块分裂、析构错堆、访问冲突全都会发生。正确做法是把单例声明为DLL内部的static全局变量,并用__declspec(dllexport)(MSVC)或__attribute__((visibility("default")))(GCC/Clang)导出其地址。
常见错误现象:std::shared_ptr在EXE中接收后,析构时崩溃——因为控制块在DLL堆分配,却由EXE的CRT释放。
- 所有模块必须链接同一份CRT(统一用
/MD或/MDd),否则堆不一致 - 不要导出类定义,只导出获取实例的C风格函数,例如
extern "C" MySingleton* GetSingleton(); - 确保
GetSingleton()内部使用static MySingleton instance;,而非new MySingleton
导出符号时必须禁用C++名称修饰
如果导出的是C++成员函数或重载函数,链接器会做名称修饰(name mangling),导致EXE端GetProcAddress或dlsym找不到符号。哪怕函数签名完全一致,MySingleton::getInstance()在不同编译器下生成的符号名也完全不同。
使用场景:Windows上用LoadLibrary加载DLL后,必须能稳定查到入口;Linux上dlsym依赖符号名精确匹配。
- 一律用
extern "C"包裹导出函数声明,强制C链接约定 - 避免导出模板函数、内联函数、带默认参数的函数——它们无法被跨模块可靠解析
- 验证符号是否导出成功:Windows可用
dumpbin /exports mylib.dll,Linux用nm -D libmylib.so
跨模块调用单例方法时禁止传递C++对象作为参数
即使单例本身安全驻留在DLL中,一旦它的接口方法接收或返回非POD的C++类(比如std::string、std::vector、自定义类),就立刻掉进内存管理陷阱。原因在于:参数在调用方栈上构造,但析构逻辑可能由DLL中的代码触发,而两者CRT堆不一致。
性能影响:看似只是传个std::string,实际触发了两次堆操作(构造+析构),且分别落在不同模块的堆上。
- 接口参数和返回值只用基本类型:
int、const char*、void*、size_t - 若需传复杂数据,改用结构体(plain old data),并由调用方分配、释放内存
- 绝对不要在DLL接口中暴露
std::shared_ptr、std::unique_ptr或任何含虚函数的类
卸载DLL前必须确保单例不再被任何模块引用
单例对象生命周期绑定DLL的.data节,DLL卸载时该内存区域会被操作系统回收。如果EXE或其他DLL仍持有指向它的指针并尝试访问,就是典型的“use-after-free”——读取访问冲突、随机崩溃。
容易被忽略的地方:插件框架中常有“热卸载”需求,但单例不支持安全热卸载,除非你彻底放弃DLL内驻留模型,改用集中式注册管理(如单独的singleton_manager.dll)。
- 主程序应在
FreeLibrary(Windows)或dlclose(Linux)前,显式调用DLL提供的CleanupSingleton()(如果提供了) - 更稳妥的做法是:单例只在进程生命周期内存在,不提供销毁接口,也不允许DLL被卸载
- 调试时可用
VirtualQuery(Windows)或mincore(Linux)检查指针地址是否仍在映射范围内
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











