动态库卸载时单例对象不会自动销毁,必须显式控制生命周期:Windows需在DllMain的DLL_PROCESS_DETACH中幂等调用destroyInstance(),Linux可用__attribute__((destructor))但受限多,跨平台最稳妥方案是导出init_singleton()和destroy_singleton()供宿主显式调用。

动态库卸载时,单例对象不会自动销毁——这不是 bug,是 C++ 标准和操作系统加载机制共同决定的事实。你写的 static Singleton& getInstance() { static Singleton instance; } 在主程序里安全,在 .dll 或 .so 里大概率失效:析构不执行、资源泄漏、后续访问变悬垂指针、甚至段错误。唯一可靠的做法是放弃“等待自动析构”,改用显式生命周期控制。
Windows 下必须在 DllMain 的 DLL_PROCESS_DETACH 中调用幂等的 destroyInstance()
这是 Windows 平台最直接有效的钩子,但有严格前提:
-
DllMain的lpReserved参数必须为nullptr才调用销毁逻辑,否则说明是进程退出而非主动卸载,此时 CRT 堆可能已不可用,delete会崩溃 -
destroyInstance()必须幂等:开头用std::atomic+s_destroyed{false} exchange(true)判重,避免多次delete - 不能在
destroyInstance()里调用日志、网络、文件等外部 API,这些符号可能已被卸载或其静态依赖已析构 - 销毁顺序要倒过来:先
join()线程、close()文件句柄、munmap()内存映射,最后才delete实例指针并置m_instance = nullptr
Linux 下 __attribute__((destructor)) 函数仅对 dlopen 加载的 SO 有效
这个属性不是万能的,它只在 dlclose() 被调用后触发,且不保证执行时机早于其他模块的析构:
- 编译时若启用了
-fvisibility=hidden,__attribute__((destructor))函数可能被剥离,导致清理完全不执行 - 函数体内禁止使用
std::cout、std::mutex、STL 容器等——它们的静态实例可能已销毁或尚未初始化 - 必须配合裸指针管理:用
static Singleton* instance = nullptr;替代局部静态变量,否则__attribute__((destructor))无法安全访问那个局部变量 - 推荐写法:
static void __attribute__((destructor)) so_cleanup() { if (instance) { delete instance; instance = nullptr; } }
跨平台最稳妥方案:显式导出 shutdown() 接口供宿主调用
把销毁控制权交给使用者,是最可控、最易测试的方式。所有平台都适用,且规避了系统钩子的各种限制:
- 在头文件中导出两个 C 风格函数:
extern "C" void init_singleton();和extern "C" void destroy_singleton(); -
init_singleton()内部用new分配单例(不依赖静态变量),并用std::atomic标记是否已初始化 -
destroy_singleton()内部delete实例,并置空指针;必须确保该函数被dlclose()或FreeLibrary()前显式调用 - 主程序侧必须严格配对:
dlopen→init_singleton→ 使用 →destroy_singleton→dlclose;漏掉任意一步都可能导致泄漏或二次析构
单例析构函数里只能做极轻量操作,否则极易崩溃
即使你用了手动管理,析构函数仍可能在不安全上下文中执行——比如系统正在调用 atexit 回调、或主线程已退出、信号处理中:
- 错误写法:
~Singleton() { log("shutting down"); db_pool->close_all(); }——log和db_pool此时极可能已失效 - 正确做法:析构函数只释放本类直接持有的原始资源(如
malloc内存、open()的fd),且必须用if (fd != -1) close(fd);加判空 - 所有高层逻辑(日志、网络、数据库连接池)必须在
destroy_singleton()阶段就完成释放,析构函数里不做任何非平凡操作 - 禁止用
std::shared_ptr管理单例本身——它的控制块析构会触发delete,而delete可能调用已被卸载的operator delete替代实现
真正容易被忽略的是:销毁逻辑不能依赖任何跨模块状态,哪怕是一行 std::cout 都可能因 libc 卸载而触发段错误。安全边界非常窄,越早把资源释放动作前移到显式接口中,越不容易踩坑。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











