Meyers单例在DLL中会创建多个实例,因各模块有独立数据段;应改用DLL内全局静态变量并显式导出符号,或用std::call_once+DLL内静态指针实现线程安全懒加载。

跨 DLL 调用时,static 局部变量单例会失效——不是线程不安全,而是根本就不是同一个实例。
为什么 Meyers 单例在 DLL 里会创建多个实例
看似安全的 static Singleton instance; 在 DLL 中被不同模块(如 EXE 和插件 DLL)各自调用时,会各自初始化一份。因为每个模块有独立的数据段和符号表,编译器为每个模块生成了独立的静态存储位置。
- 现象:主程序和插件中调用
getInstance()返回不同地址;析构函数被调两次;资源重复初始化崩溃 - 根源:C++11 的“Magic Static”只保证「单个翻译单元内」首次调用线程安全,不解决「跨模块共享同一对象」问题
- 关键区别:
static局部变量 → 每个模块私有;static全局变量 → 可通过导出符号被所有模块共用
正确做法:DLL 内全局静态对象 + 显式导出符号
把单例实例声明为 DLL 内部的全局静态变量,并用 __declspec(dllexport)(MSVC)或 __attribute__((visibility("default")))(GCC/Clang)导出其地址,确保所有模块访问的是同一内存地址。
-
Singleton.h中用宏控制导出/导入:#define MYDLL_API __declspec(dllexport)(构建 DLL 时)或__declspec(dllimport)(调用方) -
Singleton.cpp中定义:static MySingleton g_instance;—— 必须是全局静态,不能是局部静态 -
getInstance()直接返回g_instance引用,不 new、不指针、不shared_ptr - 构造函数在 DLL 加载时执行(饿汉式),天然线程安全,但无法延迟初始化
需要懒加载?用 std::call_once + DLL 内静态指针
若构造开销大或依赖运行时配置,必须延迟初始化,则放弃局部静态变量,改用显式同步机制,但所有状态仍需驻留在 DLL 内部。
立即学习“C++免费学习笔记(深入)”;
- 在 DLL 的 .cpp 文件中定义:
static std::once_flag s_flag;和static MySingleton* s_instance = nullptr; -
getInstance()中调用std::call_once(s_flag, []{ s_instance = new MySingleton(); }); - 禁止把
std::once_flag放头文件里,否则各模块会拥有自己的副本,失去同步意义 - 析构需手动管理(如 DLL 卸载时触发),或改用
std::unique_ptr配合 DLL 生命周期钩子
绝对不要做的事
这些写法看似合理,实则埋雷:
- 在头文件里写
static Singleton& getInstance() { static Singleton inst; return inst; }—— 各模块编译后各有一份 - 用
std::shared_ptr<Singleton>托管并导出工厂函数 —— 控制块分裂,引用计数失效,可能 double-delete - 导出类本身却没统一编译器/运行时(如 /MD vs /MT 不一致)—— ABI 不兼容,虚表错位、内存布局异常
- 依赖模板类导出(如
template class MYDLL_API Singleton<Config>;)—— STL 实例化细节跨模块不可靠
最易被忽略的一点:即使你用了全局静态变量,如果没加 __declspec(dllexport) 或链接时没引入对应 .lib,链接器会悄悄生成一个本地副本——你根本不会报错,但单例语义已经崩了。


















