【发布时间】:2008-09-16 18:56:21
【问题描述】:
假设我用 C++ 编写了一个 DLL,并声明了一个具有非平凡析构函数的类的全局对象。 DLL卸载时会调用析构函数吗?
【问题讨论】:
假设我用 C++ 编写了一个 DLL,并声明了一个具有非平凡析构函数的类的全局对象。 DLL卸载时会调用析构函数吗?
【问题讨论】:
在 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 会让您非常头疼。最好的解决方案是打破循环依赖。
P.S.:请注意,在 C++ 中,构造函数可以抛出,并且您不希望在 DLL 加载过程中出现异常,因此请确保您的全局对象在没有非常非常好的理由的情况下不会使用异常。由于正确编写的析构函数无权抛出,因此在这种情况下,DLL 卸载应该没问题。
【讨论】:
微软的这个页面详细介绍了 DLL 初始化和全局变量的销毁:
http://msdn.microsoft.com/en-us/library/988ye33t.aspx
【讨论】:
如果您想查看链接 .dll 时执行的实际代码,请查看%ProgramFiles%\Visual Studio 8\vc\crt\src\dllcrt0.c。
根据检查,当 dll CRT 维护的内部引用计数为零时,将通过 _cexit() 调用析构函数。
【讨论】:
它应该在应用程序结束或 DLL 被卸载时调用,以先到者为准。请注意,这在某种程度上取决于您正在编译的实际运行时。
此外,请注意非平凡的析构函数,因为存在时间和顺序问题。您的 DLL 可能会在您的析构函数所依赖的 DLL 之后被卸载,这显然会导致问题。
【讨论】:
在扩展名为 *.exe 的 windows 二进制图像文件中,*.dll 位于PE format 此类文件具有入口点。您可以使用诸如
之类的 dumpbin 工具查看它dumpbin /headers dllname.dll
如果您使用 Microsoft 的 C 运行时,那么您的入口点将类似于 *CRTStartup 或 *DllMainCRTStartup
此类函数执行 c 和 c++ 运行时的初始化,并将执行分别委托给 (main, WinMain) 或 DllMain。
如果你使用微软的VC编译器,那么你可以在你的VC目录中查看这个函数的源代码:
DllMainCRTStartup 进程在正常情况下,当它在 dll 卸载期间检索通知 DLL_PROCESS_DETACH 时,所有事情都需要从 .data 部分初始化/取消初始化您的全局变量。例如:
【讨论】:
当调用带有 fdwReason = DLL_PROCESS_DETACH 参数的 DllMain 时,意味着 DLL 已被应用程序卸载。这是调用全局/静态对象的析构函数之前的时间。
【讨论】: