分类: C/C++
2008-03-18 14:05:00
这个月是我的专栏11周年纪念以及新标题:“C++ At Work”的开幕式。同时我们还新增了一个新的双月专栏:“”,这个专栏由我的伙伴,C++ 大师级人物之一—— Stan Lipman 主持。Stan 将更多地涉及纯粹的 C++/CLI 语言方面(他会告诉你更多这方面的东西),而我将继续一如既往地编写 C++ 日常应用以及现实中的MFC(和目前的托管扩展)Windows 编程方面的文章。Stan 的新专栏描绘的是公认的当今最为活跃的主流编程语言之一的 Microsoft C++(译者:即微软的 C++ ),将在这个栏目中为我们的读者提供更多的 C++ 内容。 John E. Noël 被人抓住了小辫子,真的让我好尴尬!你说的完全正确,在我去年11月的代码中有一个bug。在那篇文章中,我描述了如何实现定制的“打开”对话框(CPersistOpenDlg),使之能记住不同用户会话的视图模式。也就是说,如果用户在“打开”对话框中选择缩略图,那么下次再运行此程序时,该对话框便会以缩略图模式打开。我是通过子类化一个特殊的窗口 SHELLDLL_DefView 来现实的,“打开”对话框使用该窗口显示文件和文件夹。我用 Spy++ 发现这个神奇的命令 IDs 来设置视图模式,同时我还示范了用 LVM_GETITEMSPACING 来区分图标模式和缩略图模式——两者从 LVM_GETVIEW/GetView 返回的都是 LV_VIEW_ICON。任何时候,只要你存储窗口状态,不管是大小/位置,还是视图模式,在窗口销毁之前的 WM_DESTROY/OnDestroy 处理例程中做这项工作是很自然的事情。那也是我在 CListViewShellWnd 中要做的事,它是为处理 SHELLDLL_DefView 而建立的一个 MFC 类: void CListViewShellWnd::OnDestroy() { m_lastViewMode = GetViewMode(); // 记住当前视图模式 } 当对话框被销毁时,其对象(CPersistOpenDlg)便在构造函数中将 m_lastViewMode 写入用户配置文件。过程就是如此——它是一种做这类事情的常规方式。但是正像 John 发现的那样,“打开”对话框在用户关闭它时并没有销毁外壳视图;它每次都是在用户转到另一个文件夹时销毁的。要改变文件清单的显示,唯一的方法是去全部从头再建立一次。(现在你知道为什么“打开”对话框后,进入另一个文件夹是如此之慢了)。为了了解文件夹视图的销毁,你只要添加一个 TRACE 即可: void CListViewShellWnd::OnDestroy() { TRACE(_T("CListViewShellWnd::OnDestroy\n")); ... } 现在运行“打开”对话框并切换文件夹。已经足够了,看一下文件夹视图便知了。CListViewShellWnd 忠实地保存了其视图模式,但当用户改变文件夹时,“打开”对话框销毁了它的 SHELLDLL_DefView,从而导致 MFC (在 CWnd::OnNcDestroy 中)对之进行自动反子类化(unsubclass)并将 m_hWnd 置为NULL。此后“打开”对话框创建新的 SHELLDLL_DefView,然而此时 CPersistOpenDlg 再也看不到这个 SHELLDLL_DefView,所以当 CPersistOpenDlg 将视图模式写入用户配置文件时,它写入的是用户改变文件夹之前的老数据。 void CListViewShellWnd::OnDestroy() { m_lastViewMode = GetViewMode(); // 与以前一样,没有变化 m_pDialog->PostMessage(MYWM_POSTINIT); // 重新发送初始化消息 } 现在,如果“打开”对话框处于任何原因决定要销毁其文件夹视图(切换文件夹或者Red Sox),CListViewShellWnd 都会发送初始化消息,从而使得我的对话框重新子类化此新的文件夹视图。很聪明,是不是?这里是 Post MYWM_POSTINIT 消息,而不是 Send,这一点很重要,所以“打开”对话框可以在 CPersistOpenDlg::OnPostInit 子类化它之前创建新的文件夹视图。之所以在 CListViewShellWnd::OnDestroy 中修复此问题,是要保证能够抓住所有“打开”对话框可能销毁其文件夹视图的地方,并且还有一个好处是不需要任何新的事件处理例程。绝对不要画蛇添足,以免遭受隐晦 bug 的困扰。 Jimmy Preston 当编程已变得如此容易,你的 Wendy 姑妈用 C# 编写着 Web 服务程序,你从容地做着自己的事情,突然天翻地覆,你四脚朝天。幸运的是,“将C++托管扩展项目从纯粹的中间语言转换成混合模式”描述了碰到这种情况时的解决方案。你可以在 MSDN 库中找到这篇文章。但鉴于你不是唯一一个遭受这种打击的程序员,所以本文中我再对上述文章的内容做一些强调,仔细分析一下幕后所发生的事情。你必须深入研究 DLLs,这对你来说没有什么坏处。暂且不说 C++,我们先从普通老式的 C DLL 开始,C DLL 并不是什么特别的东西,只不过是一个应用程序可以调用的函数集合。记住:从本质上讲,DLL 就是一个在运行时链接的子例程库,与编译时链接相对(此即 DLL被称为“动态”之所在)。与每个 C 程序都有 main 入口函数一样,DLL 都有一个入口函数叫 DllMain(正像你所看到的,DllMain 并不是必须的,但一般都会有这样一个入口函数)。DllMain 的唯一作用是为你提供一个进行初始化操作的地方。假设你要创建所有函数都要使用的全局状态。那么可以在 DllMain 中做。只要某个进程附加到你的DLL或从你的DLL分离,那么系统便会调用 DllMain。Figure 2 展示了 DllMain 的基本结构。 翻开 C++ 之前几年的日历,一切都是很顺利的,随着 C++ 的出现,现在的 DLL 有了类。假设你有一个象下面这样的类 Bobble: class Bobble { public: Bobble() { /* create */ } ~Bobble() { /* destroy */ } }; 假设你的 Bobble DLL 定义了一个全局静态实例: Bobble MyBobble; MyBobble 是一个全局对象,所有的函数都可以使用它,就像 MFC 中的 theApp。不知何故,编译器现在必须安排应用程序调用 bobble.dll 中的任何函数之前先调用 Bobble 构造函数。这在 C 语言中是绝不会有的事,在 C 中静态初始化的唯一方法如下: int GlobalVal=0; 也就是说,将原始数据类型初始化为一个常量值,编译器会进行自身管理。但现在的初始化需要调用运行时执行的函数,而不是在编译时。在 C++ 中,你甚至可以编写下面这样的代码通过某个函数来初始化一个整型: UINT WM_MYFOOMSG = RegisterWindowMessage("MYFOOMSG"); 那么谁来调用这些函数?何时调用?答案是:启动代码(startup code)在调用 DllMain 之前。你也许认为一切都是从 DllMain 开始的,但在此之前,C 运行时 DLL 启动代码调用了你的构造函数。如果你深入 crtdll.c 文件看看 CRT DLL 的初始化序列(这个文件位于\VS.NET\VC7\crt\src目录),你会发现下面的函数: BOOL WINAPI _DllMainCRTStartup(...) { if ( /* DLL_PROCESS_ATTACH */ ) { _CRT_INIT(...); // initialize CRT including ctors DllMain(...); } ... } 我对所发生的重要细节进行了简化:_DllMainCRTStartup 调用另一个 crtdll 函数,_CRT_INIT,然后调用 DllMain。_CRT_INIT 初始化 C 运行时,然后调用你的所有静态对象的构造函数。_CRT_INIT 是如何知道调用哪个构造函数的呢?是编译器产生了一个列表。所以当应用程序调用你的 C++ DLL 时,其加载顺序如下:
如果你使用 MFC,你甚至都不知道 DllMain 的存在,因为 MFC 为你提供了这个入口。在 MFC 中,一切都是从 CWinApp::InitInstance 开始的。而且是唯一的一个入口,MFC 用它自己的 DllMain 实现对它的调用。 #include <_vcclrit.h> BOOL __declspec(dllexport) MyDllInit() { return __crt_dll_initialize(); } BOOL__declspec(dllexport) MyDllTerm() { return __crt_dll_terminate(); } 其实并不难,是吗?如果你打开 _vcclrit.h 文件,看看过去那些粗糙的多线程处理代码,你会发现 __crt_dll_initialize 所做的工作无外乎调用 _DllMainCRTStartup(DLL_ PROCESS_ ATTACH)。同样,__crt_dll_terminate 则调用 _DllMainCRTStartup (DLL_PROCESS_DETACH)。如果你用 C++ 编写代码,你不会留意丢失的 Boolean 返回码,你可以写一个自动初始化类,用其构造函数/析构函数封装 __crt_dll_xxx 调用,因此,应用程序只要在全局范围的某个地方创建一个自动初始化类对象的实例即可: CMyLibInit initlib; // in main app 注意 CMyLibInit 只能适用于某个 exe 使用 DLL 的情况;如果你的客户端是另外一个DLL,你试图想在该DLL的全局范围内实例化 CMyLibInit 的话,在加载器锁定的情况下,__crt_dll_initialize 会被再次调用,从而潜在地触发同样的你正试图避免的 DLL-死锁问题。为了令从另外一个本地 DLL 初始化混合 DLL,你必须找到一个合适的地方来调用混合 DLL 的初始化/终止函数——例如,如果本地DLL是 COM 对象,那么可以在 DllGetClassObject/DllCanUnloadNow 中调用。否则你还需要从本地DLL导出另外一层初始化/终止函数,而本地DLL则调用构造函数或者加载器锁定情况下的 DllMain。 CManLibInit::CManLibInit() { TRACE(...); // Oops! __crt_dll_initialize(); } 结果我的程序崩溃了,原因是 MFC 的跟踪机制还没有被初始化。而这正是 __crt_dll_initialize 要做的事情。 |
作者简介 Paul DiLascia 是一名自由作家,顾问和 Web/UI 设计者。他是《Writing Reusable Windows Code in C++》书(Addison-Wesley, 1992)的作者。通过 可以获得更多了解。 |
本文出自 的 期刊,可通过当地报摊获得,或者最好是 |