使用 MFC 可以生成两类 DLL : MFC 扩展 DLL 和常规 DLL 。常规 DLL 又分为两类:动态链接 (dynamically linked) 和静态链接 (statically linked) 。关于这两种 DLL 的区别和简单的用法已经在前面的博文中总结过,本文主要针对以下三个方面进一步地讨论关于这两类 DLL 的用法和注意事项。
n 构建 DLL
n 在客户程序中使用 DLL
n DLL 与 MFC 和编译器的兼容相关的一些常见问题
From: CodeGuru Visual C++ 编程精粹(wcdj借于图书馆发现此书翻译的不是很好,将主要精髓整理如下)
n 构建 DLL
通过 AppWizard 可以生成一个不完成任何实质性任务的 DLL 。新的 DLL 能编译,但由于它没有导出任何类或函数,所以仍然不能发挥作用。所以我们的任务就是:
² 给 DLL 添加功能,使之发挥作用;
² 修改客户应用程序来运用这个 DLL ;
我们可以完成下面的工作:
1) 导出类
一旦结束了 AppWizard ,然后就可以从另一个项目添加 .cpp 和 .h ,来给 DLL 添加类,或者在当前项目中从头开始创建它们。为了导出类,在类声明中添加“ __declspec(dllexport) ”,如下所示:
如果创建的是 MFC 扩展 DLL ,可使用宏 AFX_EXT_CLASS :
还有其他途径来导出类,但以上办法最容易。
2) 导出变量、导出常量和导出对象
如果不导出整个类,也可以让 DLL 导出一个变量、常量或对象。导出一个变量或常量,按如下方式声明:
需要导出一个常量时,必须使用定义符 extern 。否则,会得到链接错误。
可以按相同的方式声明和导出一个类对象:
注意: 如果客户程序识别这个类而且有自己的头文件,则只能导出一个类对象。如果在 DLL 中创建一个新类,客户程序不借助头文件,就不能识别它。
当你导出一个变量或对象时,载入此 DLL 的每个客户程序都将获得自己的拷贝。于是,如果两个不同的应用程序使用同一个 DLL ,一个应用程序所做的修改不会影响另一个应用程序。
务必记住: 只能导出处于 DLL 中具有全局作用域的对象和变量。当局部对象和变量越出作用域时,它们就会消亡。因此,如果 DLL 包含如下代码,就不会正常工作:
一旦对象和变量越出作用域,它们就不复存在了。
3) 导出函数
导出函数与导出对象或变量相似。只要将“ __declspec(dllexport) ”放到函数原型开头:
如果创建的是常规 DLL ,它将提供 C 写成的客户程序使用,则函数声明应如下所示:
注意: 如果创建的是一个动态链接到 MFC 代码库 DLL 的常规 DLL ,则必须插入宏 AFX_MANAGE_STATE ,作为导出函数的首行。因此,函数定义应如下:
说明: 在每个常规 DLL 中,这样做没有危害。如果将你的 DLL 切换到静态链接,其中的宏只是无效而已。
注意: 只有 MFC 扩展 DLL 导出的函数才可以让参数和返回值使用 MFC 数据类型。
4) 导出指针
导出指针与导出一个变量或对象的方式相同,比如:
当然,如果声明和初始化指针,需要找到删除它的地方。
u 在扩展 DLL 中,有一个 DllMain() 函数。当客户程序附加到 DLL 上,而且再次分离时,会调用这个函数。因此,这是一种在扩展 DLL 中处理指针的可能方式:
u 在常规 DLL 中,看起来和普通 MFC 可执行文件更像。它有一个从 CwinApp 派生的对象来处理 DLL 的开关。可以使用 Class Wizard 添加 InitInstance() 和 ExitInstance() 函数。
n 在客户程序中使用 DLL
DLL 不能在自身上运行。它需要有一个客户应用程序载入它,并使用它的接口。
编译 DLL 时,编译器创建两个重要文件: .dll 文件和 .lib 文件。客户应用程序需要这两个文件。必须将它们复制到客户应用程序的项目文件夹中。
注意: 在 debug 模式下通过生成创建的 .dll 文件和 .lib 文件与在 release 模式下创建的文件是不同的。在 debug 模式下生成的客户程序时,需要 .dll 和 .lib 的 debug 版本;而在 release 模式下生成的客户程序时,则需要 .dll 和 .lib 的 release 版本。
除了 .dll 和 .lib 文件外,客户程序还需要针对导出内容 ( 导出类、函数、对象和变量 ) 的头文件。导出时,在声明中添加“ __declspec(dllexport) ”,而在导入时,需要添加“ __declspec(dllimport) ”。
记住: 如果在 DLL 中使用定义符 extern “C” ,也必须在客户程序中使用它。
为了导入整个类,必须复制整个 .h 头文件到客户程序。 DLL 和客户程序于是具有导出类的相同的头文件,不同的是,在 DLL 中是“ class __declspec(dllexport) CMyClass ”,在客户程序文件中是“ class __declspec(dllimport) CMyClass ”。如果创建的是 MFC 扩展 DLL ,在两个位置均为“ class AFX_EXT_CLASS CMyClass ”。
说明: 一旦完成了客户程序的构建,就准备将它移交给实际用户,给予他们的应该是发行版本的可执行文件以及发行版本的 DLL ,不必给予用户 .lib 文件。 .dll 文件和 .exe 文件放在同一个目录中,或者将 .dll 放在 Windows 的 System 目录下。
警告: 由于 DLL 存在一些严重的缺陷,因此 COM 和 ATL 才应运而生。 DLL 存在的主要问题有两个: (1) 通过某种版本编译器构建的 DLL 可能与另外的编译器构建的客户程序不兼容。 (2) 修改 DLL 时,可能必须重新编译客户程序,即使没有更改客户程序中的代码时也是如此。可能仍然要拷入新的 .dll 和 .lib 文件,再进行重新的编译。
在某些情况下,可以通过一些途径来避免这个问题。下文将介绍两种方法。
n DLL 与 MFC 和编译器的兼容相关的一些常见问题
DLL 是任何 MFC 程序员有用的工具,但是它们存在许多重要的限制。
Ø MFC 问题 。 DLL 必须具备正确版本的 MFC 代码库。
Ø 编译器不兼容性问题 。用一种版本的编译器来构建 DLL ,但用另一种版本的编译器构建的 App 来调用它,这时就有可能会出现问题。
Ø 重新编译问题 。比如现在构建的 DLL 导出一个名叫 CMyClass 的类,为 CMyClass 提供一个头文件的拷贝,供客户应用程序使用,并假设 CMyClass 对象的大小为 30 字节。这时,假如修改 DLL 来更改 CMyClass ,为其增加一个 int 型的私有成员变量,因此,当创建 CMyClass 类型的对象时,大小变为 34 个字节。将这个新的 DLL 发送到用户,并通知他们替换掉旧的 DLL ,于是就产生了一个问题:客户 App 期望的是 30 字节大小的对象,但是新的 DLL 创建的是 34 个字节大小的对象,这时客户 App 就会出错。
解决方案是什么?
如果上述这些问题存在完美的解决方案的话,这也许就是 COM 。但是掌握 COM 或 ATL 需要花费大量的时间和精力,运用 DLL 相对容易得多。下面介绍两种通过修改 DLL 来解决上述问题的办法:
1) 使用接口类
接口类的目标是分离需要导出的类与该类的接口。完成的途径是创建第二个类,它将作为需要导出的类的接口。于是,即使在导出类改变时,也不必重新编译客户的 App ,因为接口类保持不变。
创建一个单独的接口类,只要接口类 ( 大小 ) 不改变,就不必重新编译。但是采用这种方法仍然存在两个相对较小的问题。 (1) 首先,对于 CMyClass 中的每个公有函数和成员变量,必须在 CMyInterface 中创建一个对应的函数或变量。如果 CMyClass 中有上百个函数和变量,创建过程就会非常冗长且很容易出错。 (2) 这样做导致必须完成的过程数量增加了,客户的 App 不再直接调用 CMyClass ,相反,它调用一个调用 CMyClass 的 CMyInterface 函数,如果这是一个被客户 App 经常调用的函数,那么额外的处理时间会激增。
2) 使用创建和销毁导出类的静态函数
避免必须重新编译的另一条途径是使用静态函数来创建和销毁导出类。在创建导出类时,添加两个公有静态函数 CreateMe() 和 DestoryMe() ,使用这种技术也可以修改 CMyClass 的大小而不必重新编译客户的 App 。