假设我用 C++ 编写了一个 DLL,并声明了一个具有非平凡析构函数的类的全局对象。DLL卸载时会调用析构函数吗?
6 回答
在 Windows C++ DLL 中,所有全局对象(包括类的静态成员)将在使用 DLL_PROCESS_ATTACH 调用 DllMain 之前构建,并且在使用 DLL_PROCESS_DETACH 调用 DllMain 之后将它们销毁。
现在,您必须考虑三个问题:
0 - 当然,全局非常量对象是邪恶的(但你已经知道了,所以我将避免提及多线程、锁、上帝对象等)
1 - 不保证对象或不同编译单元(即 CPP 文件)的构建顺序,因此如果两个对象在两个不同的 CPP 中实例化,您不能希望对象 A 在 B 之前构建。如果 B 依赖于 A,这很重要。解决方案是将所有全局对象移动到同一个 CPP 文件中,因为在同一个编译单元内,对象的实例化顺序将是构造顺序(以及顺序的倒数)破坏)
2 - 在 DllMain 中有一些禁止做的事情。这些东西在构造函数中也可能是被禁止的。所以避免锁定一些东西。请参阅 Raymond Chen 关于该主题的优秀博客:
在这种情况下,延迟初始化可能会很有趣:类保持在“未初始化”状态(内部指针为 NULL,布尔值为 false,等等),直到您调用它们的方法之一,此时它们将自行初始化。如果您在 main(或 main 的后代函数之一)中使用这些对象,您会没事的,因为它们将在 DllMain 执行后被调用。
3 - 当然,如果 DLL A 中的一些全局对象依赖于 DLL B 中的全局对象,你应该非常小心 DLL 的加载顺序,以及依赖关系。在这种情况下,具有直接或间接循环依赖的 DLL 会让您非常头疼。最好的解决方案是打破循环依赖。
PS:请注意,在 C++ 中,构造函数可以抛出,并且您不希望在 DLL 加载过程中出现异常,因此请确保您的全局对象在没有非常非常好的理由的情况下不会使用异常。由于正确编写的析构函数无权抛出,因此在这种情况下,DLL 卸载应该没问题。
Microsoft 的此页面详细介绍了 DLL 初始化和全局变量的破坏:http:
//msdn.microsoft.com/en-us/library/988ye33t.aspx
如果您想查看链接 .dll 时执行的实际代码,请查看%ProgramFiles%\Visual Studio 8\vc\crt\src\dllcrt0.c
.
_cexit()
通过检查,当 dll CRT 维护的内部引用计数达到零时,将调用析构函数。
它应该在应用程序结束或 DLL 被卸载时调用,以先到者为准。请注意,这在某种程度上取决于您正在编译的实际运行时。
此外,请注意非平凡的析构函数,因为存在时间和顺序问题。您的 DLL 可能会在您的析构函数依赖的 DLL之后被卸载,这显然会导致问题。
在带有扩展名*.exe 的windows 二进制图像文件中,*.dll 为PE 格式, 此类文件具有入口点。您可以使用 dumpbin 工具查看它,例如
dumpbin /headers dllname.dll
如果您使用 Microsoft 的 C 运行时,那么您的入口点将类似于 *CRTStartup 或 *DllMainCRTStartup
此类函数执行 c 和 c++ 运行时的初始化,并将执行分别委托给 (main, WinMain) 或 DllMain。
如果您使用微软的 VC 编译器,那么您可以在您的 VC 目录中查看此函数的源代码:
- crt0.c
- dllcrt0.c
DllMainCRTStartup 在正常情况下,当它在 dll 卸载期间检索通知 DLL_PROCESS_DETACH 时,所有事情都需要从 .data 部分初始化/取消初始化您的全局变量。例如:
- 程序启动线程的main或WinMain返回控制流
- 您明确调用 FreeLibrary 并且 use-dll-counter 为零
当调用带有 fdwReason = DLL_PROCESS_DETACH 参数的 DllMain 时,意味着应用程序卸载了 DLL。这是调用全局/静态对象的析构函数之前的时间。