2012年3月20日星期二

dll 的调用

编译时,关键是要能找到.lib或.a文件,所以要将存放.lib或.a文件的路径加入到.pro文件中,比如 LIBS += -L"C:\path1\for\lib",同时将库名(也可全称)加到LIBS中,比如LIBS += -llibname.lib
运 行时,关键是能找到对应的.dll或.so文件,在windows下,此时要么将.dll文件放到可执行文件目录下,要么在环境变量PATH中将.dll 文件所在路径加进去。在linux下,则要将.so文件所在的目录加入到环境变量LD_LIBRARY_PATH路径中去(或放到那些公用库目录下,比如 /usr/lib/

2012年3月16日星期五

VC++ 问题 cannot convert parameter 1 from 'const char [21]' to 'LPCWSTR'

 选择 项目属性--> Character Set --> 选择Use Multi-Byte Character Set
 
 
This error message means that you are trying to pass a multi-byte string (const char [12]) to a function which expects a unicode string (LPCTSTR). The LPCTSTR type extends to const TCHAR*, where TCHAR ischarwhen you compile for multi-byte andwchar_tfor unicode. Since the compiler doesn't accept the char array, we can safely assume that the actual type of TCHAR, in this compilation, is wchar_t.
 
Resolution
You will have to do one of two things:
Change your project configuration to use multibyte strings. Press ALT+F7 to open the properties, and navigate to Configuration Properties > General. Switch Character Set to "Use Multi-Byte Character Set".
Indicate that the string literal, in this case "Hello world!" is of a specific encoding. This can be done through either prefixing it withL, such asL"Hello world!", or surrounding it with the generic_T("Hello world!")macro. The latter will expand to theLprefix if you are compiling for unicode (see #1), and nothing (indicating multi-byte) otherwise.
Variations
Another error message, indicating the same problem, would be:
cannot convert parameter 1 from 'const char [12]' to 'LPCWSTR'
Where LPCWSTR maps to a wchar_t pointer, regardless of your build configuration. This problem can be resolved primarily by using solution #2, but in some cases also #1. A lot of the Microsoft provided libraries, such as the Platform SDK, have got two variations of each function which takes strings as parameters. In case of a unicode build, the actual functions are postfixed W, such as the MessageBoxW seen above. In case of multi-byte, the function would be MessageBoxA (ASCII). Which of these functions is actually used when you compile your application, depends on the setting described in resolution #1 above.

2012年3月15日星期四

转载 VC++动态链接库(DLL)编程深入浅出

1.概论
 先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。
 静态链接库与动态链接库都是共享代 码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终 EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包 含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
对动态链接库,我们还需建立如下概念:
(1)DLL 的编制与具体的编程语言及编译器无关
只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是Visual Basic、Visual C++还是Delphi。
(2)动态链接库随处可见
 我 们在Windows目录下的system32文件夹中会看到kernel32.dll、user32.dll和gdi32.dll,windows的大多 数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面;gdi32.dll中的函数则负责图形方面的操作。
 一般的程序员都用过类似MessageBox的函数,其实它就包含在user32.dll这个动态链接库中。由此可见DLL对我们来说其实并不陌生。
(3)VC动态链接库的分类
Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。
非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL 包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。
 由于本文篇幅较长,内容较多,势必需要先对阅读本文的有关事项进行说明,下面以问答形式给出。
问:本文主要讲解什么内容?
答:本文详细介绍了DLL编程的方方面面,努力学完本文应可以对DLL有较全面的掌握,并能编写大多数DLL程序。
问:如何看本文?
答:本文每一个主题的讲解都附带了源代码例程,可以随文下载(每个工程都经WINRAR压缩)。所有这些例程都由笔者编写并在VC++6.0中调试通过。
 当然看懂本文不是读者的最终目的,读者应亲自动手实践才能真正掌握DLL的奥妙。
 问:学习本文需要什么样的基础知识?
答:如果你掌握了C,并大致掌握了C++,了解一点MFC的知识,就可以轻松地看懂本文。
2.静态链接库
对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。

图1 建立一个静态链接库
如图1,在VC++6.0中new一个名称为libTest的static library工程(单击此处下载本工程附件 ),并新建lib.h和lib.cpp两个文件,lib.h和lib.cpp的源代码如下:


 编译这个工程就得到了一个.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件和.lib文件提交给用户后,用户就可以直接使用其中的add函数了。
标准Turbo C2.0中的C库函数(我们用来的scanf、printf、memcpy、strcpy等)就来自这种静态库。
 下面来看看怎么使用这个库,在libTest工程所在的工作区内new一个libCall工程。libCall工程仅包含一个main.cpp文件,它演示了静态链接库的调用方法,其源代码如下:


  静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中#pragma comment( lib , "..debuglibTest.lib" )的意思是指本文件生成的.obj文件应与libTest.lib一起连接。
如果不用#pragma comment指定,则可以直接在VC++中设置,如图2,依次选择tools、options、directories、library files菜单或选项,填入库文件路径。图2中加红圈的部分为我们添加的libTest.lib文件的路径。

图2 在VC中设置库文件路径
这个静态链接库的例子至少让我们明白了库函数是怎么回事,它们是哪来的。我们现在有下列模糊认识了:
 (1)库不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行;
(2)库提供一些可以给别的程序调用的东东,别的程序要调用它必须以某种方式指明它要调用之。
 以上从静态链接库分析而得到的对库的懵懂概念可以直接引申到动态链接库中,动态链接库与静态链接库在编写和调用上的不同体现在库的外部接口定义及调用方式略有差异。
3.库的调试与查看
 在具体进入各类DLL的详细阐述之前,有必要对库文件的调试与查看方法进行一下介绍,因为从下一节开始我们将面对大量的例子工程。
 由 于库文件不能单独执行,因而在按下F5(开始debug模式执行)或CTRL+F5(运行)执行时,其弹出如图3所示的对话框,要求用户输入可执行文件的 路径来启动库函数的执行。这个时候我们输入要调用该库的EXE文件的路径就可以对库进行调试了,其调试技巧与一般应用工程的调试一样。

图3 库的调试与“运行”
 通常有比上述做法更好的调试途径,那就是将库工程和应用工程(调用库的工程)放置在同一VC工作区,只对应用工程进行调试,在应用工程调用库中函 数的语句处设置断点,执行后按下F11,这样就单步进入了库中的函数。第2节中的libTest和libCall工程就放在了同一工作区,其工程结构如图 4所示。

图4 把库工程和调用库的工程放入同一工作区进行调试
上述调试方法对静态链接库和动态链接库而言是一致的。所以本文提供下载的所有源代码中都包含了库工程和调用库的工程,这二者都被包含在一个工作区内,这是笔者提供这种打包下载的用意所在。
动态链接库中的导出接口可以使用Visual C++的Depends工具进行查看,让我们用Depends打开系统目录中的user32.dll,看到了吧?红圈内的就是几个版本的MessageBox了!原来它真的在这里啊,原来它就在这里啊!

图5 用Depends查看DLL
当然Depends工具也可以显示DLL的层次结构,若用它打开一个可执行文件则可以看出这个可执行文件调用了哪些DLL。
 好,让我们正式进入动态链接库的世界,先来看看最一般的DLL,即非MFC DLL






上节给大家介绍了静态链接库与库的调试与查看(动态链接库(DLL)编程深入浅出(一) ),本节主要介绍非MFC DLL。
4.非MFC DLL
 4.1一个简单的DLL
 第2节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。
 如图6,在VC++中new一个Win32 Dynamic-Link Library工程dllTest(单击此处下载本工程附件 )。注意不要选择MFC AppWizard(dll),因为用MFC AppWizard(dll)建立的将是第5、6节要讲述的MFC 动态链接库。

图6 建立一个非MFC DLL
在建立的工程中添加lib.h及lib.cpp文件,源代码如下:

  1. /* 文件名:lib.h */  
  2. #ifndef LIB_H  
  3. #define LIB_H  
  4. extern "C" int __declspec(dllexport)add(int x, int y);  
  5. #endif  
  6. /* 文件名:lib.cpp */  
  7. #include "lib.h"  
  8. int add(int x, int y)  
  9. {  
  10. return x + y;  
  11. }  

  与第2节对静态链接库的调用相似,我们也建立一个与DLL工程处于同一工作区的应用工程dllCall,它调用DLL中的函数add,其源代码如下:

  1. #include <stdio.h>  
  2. #include <windows.h>  
  3. typedef int(*lpAddFun)(intint); //宏定义函数指针类型  
  4. int main(int argc, char *argv[])  
  5. {  
  6. HINSTANCE hDll; //DLL句柄  
  7. lpAddFun addFun; //函数指针  
  8. hDll = LoadLibrary("..DebugdllTest.dll");  
  9. if (hDll != NULL)  
  10. {  
  11. addFun = (lpAddFun)GetProcAddress(hDll, "add");  
  12. if (addFun != NULL)  
  13. {  
  14. int result = addFun(2, 3);  
  15. printf("%d", result);  
  16. }  
  17. FreeLibrary(hDll);  
  18. }  
  19. return 0;  
  20. }  

   分析上述代码,dllTest工程中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了 __declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数。DLL内的函数分为两种:
     (1)DLL导出函数,可供应用程序调用;
(2) DLL内部函数,只能在DLL程序使用,应用程序无法调用它们。
 而应用程序对本DLL的调用和对第2节静态链接库的调用却有较大差异,下面我们来逐一分析。
首先,语句typedef int ( * lpAddFun)(int,int)定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpAddFun的实例addFun;
其次,在函数main中定义了一个DLL HINSTANCE句柄实例hDll,通过Win32 Api函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll;
再次,在函数main中通过Win32 Api函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun。经由函数指针addFun进行了对DLL中add函数的调用;
 最后,应用工程使用完DLL后,在函数main中通过Win32 Api函数FreeLibrary释放了已经加载的DLL模块。
 通过这个简单的例子,我们获知DLL定义和调用的一般概念:
(1)DLL中需以某种特定的方式声明导出函数(或变量、类);
(2)应用工程需以某种特定的方式调用DLL的导出函数(或变量、类)。
下面我们来对“特定的方式进行”阐述。
4.2 声明导出函数
DLL中导出函数的声明有两种方式:一种为4.1节例子中给出的在函数声明中加上__declspec(dllexport),这里不再举例说明;另外 一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
下面的代码演示了怎样同.def文件将函数add声明为DLL导出函数(需在dllTest工程中添加lib.def文件):
; lib.def : 导出DLL函数
LIBRARY dllTest
EXPORTS
add @ 1
  .def文件的规则为:
(1)LIBRARY语句说明.def文件相应的DLL;
 (2)EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);
 (3).def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。
由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。
4.3 DLL的调用方式
 在4.1节的例子中我们看到了由“LoadLibrary-GetProcAddress-FreeLibrary”系统Api提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式称为DLL的动态调用。
动态调用方式的特点是完全由编程者用 API 函数加载和卸载 DLL,程序员可以决定 DLL 文件何时加载或不加载,显式链接在运行时决定加载哪个 DLL 文件。
 与 动态调用方式相对应的就是静态调用方式,“有动必有静”,这来源于物质世界的对立统一。“动与静”,其对立与统一竟无数次在技术领域里得到验证,譬如静态 IP与DHCP、静态路由与动态路由等。从前文我们已经知道,库也分为静态库与动态库DLL,而想不到,深入到DLL内部,其调用方式也分为静态与动态。 “动与静”,无处不在。《周易》已认识到有动必有静的动静平衡观,《易.系辞》曰:“动静有常,刚柔断矣”。哲学意味着一种普遍的真理,因此,我们经常可以在枯燥的技术领域看到哲学的影子。
 静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。下面我 们来看看静态调用的例子(单击此处下载本工程附件 ),将编译dllTest工程所生成的.lib和.dll文件拷入dllCall工程所在的路径,dllCall执行下列代码:#pragma comment(lib,"dllTest.lib")
//.lib文件中仅仅是关于其对应DLL文件中函数的重定位信息

  1. extern "C" __declspec(dllimport) add(int x,int y);  
  2. int main(int argc, char* argv[])  
  3. {  
  4. int result = add(2,3);  
  5. printf("%d",result);  
  6. return 0;  
  7. }  

  由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:
 (1)告诉编译器与DLL相对应的.lib文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib")就是起这个作用。
 程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。
(2)声明导入函数,extern "C" __declspec(dllimport) add(int x,int y)语句中的__declspec(dllimport)发挥这个作用。
 静 态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序 中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。
4.4 DllMain函数
 Windows在加载DLL的时候,需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。 在前面的例子中,DLL并没有提供DllMain函数,应用工程也能成功引用DLL,这是因为Windows在找不到DllMain的时候,系统会从其它 运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着DLL可以放弃DllMain函数。
根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。
我们来看一个DllMain函数的例子(单击此处下载本工程附件 )。
  1. BOOL APIENTRY DllMain( HANDLE hModule,  
  2. DWORD ul_reason_for_call,  
  3. LPVOID lpReserved  
  4. )  
  5. {  
  6. switch (ul_reason_for_call)  
  7. {  
  8. case DLL_PROCESS_ATTACH:  
  9. printf(" process attach of dll");  
  10. break;  
  11. case DLL_THREAD_ATTACH:  
  12. printf(" thread attach of dll");  
  13. break;  
  14. case DLL_THREAD_DETACH:  
  15. printf(" thread detach of dll");  
  16. break;  
  17. case DLL_PROCESS_DETACH:  
  18. printf(" process detach of dll");  
  19. break;  
  20. }  
  21. return TRUE;  
  22. }  

   DllMain函数在DLL被加载和卸载时被调用,在单个线程启动和终止时,DLLMain函数也被调用,ul_reason_for_call指明了 被调用的原因。原因共有4种,即PROCESS_ATTACH、PROCESS_DETACH、THREAD_ATTACH和 THREAD_DETACH,以switch语句列出。
来仔细解读一下DllMain的函数头BOOL APIENTRY DllMain( HANDLE hModule, WORD ul_reason_for_call, LPVOID lpReserved )。
APIENTRY被定义为__stdcall,它意味着这个函数以标准Pascal的方式进行调用,也就是WINAPI方式;
进程中的每个DLL模块被全局唯一的32字节的HINSTANCE句柄标识,只有在特定的进程内部有效,句柄代表了DLL模块在进程虚拟空间中的起始地 址。在Win32中,HINSTANCE和HMODULE的值是相同的,这两种类型可以替换使用,这就是函数参数hModule的来历。
执行下列代码:
  1. hDll = LoadLibrary("..DebugdllTest.dll");  
  2. if (hDll != NULL)  
  3. {  
  4. addFun = (lpAddFun)GetProcAddress(hDll, MAKEINTRESOURCE(1));  
  5. //MAKEINTRESOURCE直接使用导出文件中的序号  
  6. if (addFun != NULL)  
  7. {  
  8. int result = addFun(2, 3);  
  9. printf(" call add in dll:%d", result);  
  10. }  
  11. FreeLibrary(hDll);  
  12. }  

  我们看到输出顺序为:
process attach of dll
call add in dll:5
process detach of dll
这一输出顺序验证了DllMain被调用的时机。
代码中的GetProcAddress ( hDll, MAKEINTRESOURCE ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在MAKEINTRESOURCE ( 1 ),MAKEINTRESOURCE是一个通过序号获取函数名的宏,定义为(节选自winuser.h):
    #define MAKEINTRESOURCEA(i) (LPSTR)((DWORD)((WORD)(i)))
    #define MAKEINTRESOURCEW(i) (LPWSTR)((DWORD)((WORD)(i)))
    #ifdef UNICODE
    #define MAKEINTRESOURCE MAKEINTRESOURCEW
    #else
    #define MAKEINTRESOURCE MAKEINTRESOURCEA
  4.5 __stdcall约定
 如果通过VC++编写的DLL欲被其他语言编 写的程序调用,应将函数的调用方式声明为__stdcall方式,WINAPI都采用这种方式,而C/C++缺省的调用方式却为__cdecl。 __stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而 __cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。
Windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):
#define CALLBACK __stdcall //这就是传说中的回调函数
#define WINAPI __stdcall //这就是传说中的WINAPI
#define WINAPIV __cdecl
#define APIENTRY WINAPI //DllMain的入口就在这里
#define APIPRIVATE __stdcall
#define PASCAL __stdcall
  在lib.h中,应这样声明add函数:
int __stdcall add(int x, int y);
 在应用工程中函数指针类型应定义为:
typedef int(__stdcall *lpAddFun)(int, int);
若在lib.h中将函数声明为__stdcall调用,而应用工程中仍使用typedef int (* lpAddFun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图7所示的对话框。
  图7 调用约定不匹配时的运行错误
图8中的那段话实际上已经给出了错误的原因,即“This is usually a result of …”。
单击此处下载__stdcall调用例子工程源代码附件
4.6 DLL导出变量
DLL定义的全局变量可以被调用进程访问;DLL也可以访问调用进程的全局数据,我们来看看在应用工程中引用DLL中变量的例子(单击此处下载本工程附件 )。
  1. /* 文件名:lib.h */  
  2. #ifndef LIB_H  
  3. #define LIB_H  
  4. extern int dllGlobalVar;  
  5. #endif  
  6. /* 文件名:lib.cpp */  
  7. #include "lib.h"  
  8. #include <windows.h>  
  9. int dllGlobalVar;  
  10. BOOL APIENTRY DllMain(HANDLE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)  
  11. {  
  12. switch (ul_reason_for_call)  
  13. {  
  14. case DLL_PROCESS_ATTACH:  
  15. dllGlobalVar = 100; //在dll被加载时,赋全局变量为100  
  16. break;  
  17. case DLL_THREAD_ATTACH:  
  18. case DLL_THREAD_DETACH:  
  19. case DLL_PROCESS_DETACH:  
  20. break;  
  21. }  
  22. return TRUE;  
  23. }  
  24. ;文件名:lib.def  
  25. ;在DLL中导出变量  
  26. LIBRARY "dllTest"  
  27. EXPORTS  
  28. dllGlobalVar CONSTANT  
  29. ;或dllGlobalVar DATA  
  30. GetGlobalVar  

 从lib.h和lib.cpp中可以看出,全局变量在DLL中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的EXPORTS后添加:
变量名 CONSTANT   //过时的方法

变量名 DATA     //VC++提示的新方法
在主函数中引用DLL中定义的全局变量:
  1. #include <stdio.h>  
  2. #pragma comment(lib,"dllTest.lib")  
  3. extern int dllGlobalVar;  
  4. int main(int argc, char *argv[])  
  5. {  
  6. printf("%d ", *(int*)dllGlobalVar);  
  7. *(int*)dllGlobalVar = 1;  
  8. printf("%d ", *(int*)dllGlobalVar);  
  9. return 0;  
  10. }  

 特别要注意的是用extern int dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从* (int*)dllGlobalVar可以看出。因此在采用这种方式引用DLL全局变量时,千万不要进行这样的赋值操作:
dllGlobalVar = 1;
 其结果是dllGlobalVar指针的内容发生变化,程序中以后再也引用不到DLL中的全局变量了。
 在应用工程中引用DLL中全局变量的一个更好方法是:
  1. #include <stdio.h>  
  2. #pragma comment(lib,"dllTest.lib")  
  3. extern int _declspec(dllimport) dllGlobalVar; //用_declspec(dllimport)导入  
  4. int main(int argc, char *argv[])  
  5. {  
  6. printf("%d ", dllGlobalVar);  
  7. dllGlobalVar = 1; //这里就可以直接使用, 无须进行强制指针转换  
  8. printf("%d ", dllGlobalVar);  
  9. return 0;  
  10. }  

  通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。
4.7 DLL导出类
 DLL中定义的类可以在应用工程中使用。
下面的例子里,我们在DLL中定义了point和circle两个类,并在应用工程中引用了它们(单击此处下载本工程附件 )。
  1. //文件名:point.h,point类的声明  
  2. #ifndef POINT_H  
  3. #define POINT_H  
  4. #ifdef DLL_FILE  
  5. class _declspec(dllexport) point //导出类point  
  6. #else  
  7. class _declspec(dllimport) point //导入类point  
  8. #endif  
  9. {  
  10. public:  
  11. float y;  
  12. float x;  
  13. point();  
  14. point(float x_coordinate, float y_coordinate);  
  15. };  
  16. #endif  
  17. //文件名:point.cpp,point类的实现  
  18. #ifndef DLL_FILE  
  19. #define DLL_FILE  
  20. #endif  
  21. #include "point.h"  
  22. //类point的缺省构造函数  
  23. point::point()  
  24. {  
  25. x = 0.0;  
  26. y = 0.0;  
  27. }  
  28. //类point的构造函数  
  29. point::point(float x_coordinate, float y_coordinate)  
  30. {  
  31. x = x_coordinate;  
  32. y = y_coordinate;  
  33. }  
  34. //文件名:circle.h,circle类的声明  
  35. #ifndef CIRCLE_H  
  36. #define CIRCLE_H  
  37. #include "point.h"  
  38. #ifdef DLL_FILE  
  39. class _declspec(dllexport)circle //导出类circle  
  40. #else  
  41. class _declspec(dllimport)circle //导入类circle  
  42. #endif  
  43. {  
  44. public:  
  45. void SetCentre(const point ¢rePoint);  
  46. void SetRadius(float r);  
  47. float GetGirth();  
  48. float GetArea();  
  49. circle();  
  50. private:  
  51. float radius;  
  52. point centre;  
  53. };  
  54. #endif  
  55. //文件名:circle.cpp,circle类的实现  
  56. #ifndef DLL_FILE  
  57. #define DLL_FILE  
  58. #endif  
  59. #include "circle.h"  
  60. #define PI 3.1415926  
  61. //circle类的构造函数  
  62. circle::circle()  
  63. {  
  64. centre = point(0, 0);  
  65. radius = 0;  
  66. }  
  67. //得到圆的面积  
  68. float circle::GetArea()  
  69. {  
  70. return PI *radius * radius;  
  71. }  
  72. //得到圆的周长  
  73. float circle::GetGirth()  
  74. {  
  75. return 2 *PI * radius;  
  76. }  
  77. //设置圆心坐标  
  78. void circle::SetCentre(const point ¢rePoint)  
  79. {  
  80. centre = centrePoint;  
  81. }  
  82. //设置圆的半径  
  83. void circle::SetRadius(float r)  
  84. {  
  85. radius = r;  
  86. }  

  类的引用:

  1. #include "..circle.h"  //包含类声明头文件  
  2. #pragma comment(lib,"dllTest.lib");  
  3. int main(int argc, char *argv[])  
  4. {  
  5. circle c;  
  6. point p(2.0, 2.0);  
  7. c.SetCentre(p);  
  8. c.SetRadius(1.0);  
  9. printf("area:%f girth:%f", c.GetArea(), c.GetGirth());  
  10. return 0;  
  11. }  

  从上述源代码可以看出,由于在DLL的类实现代码中定义了宏DLL_FILE,故在DLL的实现中所包含的类声明实际上为:
class _declspec(dllexport) point //导出类point
{

}
  和
class _declspec(dllexport) circle //导出类circle
{

}
 而在应用工程中没有定义DLL_FILE,故其包含point.h和circle.h后引入的类声明为:
class _declspec(dllimport) point //导入类point
{

}
  和
class _declspec(dllimport) circle //导入类circle
{

}
  不错,正是通过DLL中的
class _declspec(dllexport) class_name //导出类circle 
{

}
  与应用程序中的
class _declspec(dllimport) class_name //导入类
{

}
  匹对来完成类的导出和导入的!
我们往往通过在类的声明头文件中用一个宏来决定使其编译为class _declspec(dllexport) class_name还是class _declspec(dllimport) class_name版本,这样就不再需要两个头文件。本程序中使用的是:
#ifdef DLL_FILE
class _declspec(dllexport) class_name //导出类
#else
class _declspec(dllimport) class_name //导入类
#endif
 实际上,在MFC DLL的讲解中,您将看到比这更简便的方法,而此处仅仅是为了说明_declspec(dllexport)与_declspec(dllimport)匹对的问题。
 由此可见,应用工程中几乎可以看到DLL中的一切,包括函数、变量以及类,这就是DLL所要提供的强大能力。只要DLL释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!
本章虽以VC++为平台讲解非MFC DLL,但是这些普遍的概念在其它语言及开发环境中也是相同的,其思维方式可以直接过渡。
 接下来,我们将要研究MFC规则DLL



第4节我们对非MFC DLL进行了介绍,这一节将详细地讲述MFC规则DLL的创建与使用技巧。
 另外,自从本文开始连载后,收到了一些读者的e-mail。有的读者提出了一些问题,笔者将在本文的最后一次连载中选取其中的典型问题进行解答。 由于时间的关系,对于读者朋友的来信,笔者暂时不能一一回复,还望海涵!由于笔者的水平有限,文中难免有错误和纰漏,也热诚欢迎读者朋友不吝指正!
5. MFC规则DLL
5.1 概述
MFC规则DLL的概念体现在两方面:
(1) 它是MFC的
“是MFC的”意味着可以在这种DLL的内部使用MFC;
(2) 它是规则的
 “是规则的”意味着它不同于MFC扩展DLL,在MFC规则DLL的内部虽然可以使用MFC,但是其与应用程序的接口不能是MFC。而MFC扩展DLL与应用程序的接口可以是MFC,可以从MFC扩展DLL中导出一个MFC类的派生类。
 Regular DLL能够被所有支持DLL技术的语言所编写的应用程序调用,当然也包括使用MFC的应用程序。在这种动态连接库中,包含一个从CWinApp继承下来的类,DllMain函数则由MFC自动提供。
Regular DLL分为两类:
(1)静态链接到MFC 的规则DLL
 静态链接到MFC的规则DLL与MFC库(包括MFC扩展 DLL)静态链接,将MFC库的代码直接生成在.dll文件中。在调用这种DLL的接口时,MFC使用DLL的资源。因此,在静态链接到MFC 的规则DLL中不需要进行模块状态的切换。
使用这种方法生成的规则DLL其程序较大,也可能包含重复的代码。
(2)动态链接到MFC 的规则DLL
 动态链接到MFC 的规则DLL 可以和使用它的可执行文件同时动态链接到 MFC DLL 和任何MFC扩展 DLL。在使用了MFC共享库的时候,默认情况下,MFC使用主应用程序的资源句柄来加载资源模板。这样,当DLL和应用程序中存在相同ID的资源时(即 所谓的资源重复问题),系统可能不能获得正确的资源。因此,对于共享MFC DLL的规则DLL,我们必须进行模块切换以使得MFC能够找到正确的资源模板。
我们可以在Visual C++中设置MFC规则DLL是静态链接到MFC DLL还是动态链接到MFC DLL。如图8,依次选择Visual C++的project -> Settings -> General菜单或选项,在Microsoft Foundation Classes中进行设置。
图8 设置动态/静态链接MFC DLL
5.2 MFC规则DLL的创建
我们来一步步讲述使用MFC向导创建MFC规则DLL的过程,首先新建一个project,如图9,选择project的类型为MFC AppWizard(dll)。点击OK进入如图10所示的对话框。

图9 MFC DLL工程的创建
图10所示对话框中的1区选择MFC DLL的类别。
 2区选择是否支持automation(自动化)技术, automation 允许用户在一个应用程序中操纵另外一个应用程序或组件。例如,我们可以在应用程序中利用 Microsoft Word 或Microsoft Excel的工具,而这种使用对用户而言是透明的。自动化技术可以大大简化和加快应用程序的开发。
 3区选择是否支持Windows Sockets,当选择此项目时,应用程序能在 TCP/IP 网络上进行通信。 CWinApp派生类的InitInstance成员函数会初始化通讯端的支持,同时工程中的StdAfx.h文件会自动include <AfxSock.h>头文件。
添加socket通讯支持后的InitInstance成员函数如下:

  1. BOOL CRegularDllSocketApp::InitInstance()  
  2. {  
  3. if (!AfxSocketInit())  
  4. {  
  5. AfxMessageBox(IDP_SOCKETS_INIT_FAILED);  
  6. return FALSE;  
  7. }  
  8. return TRUE;  
  9. }  

  4区选择是否由MFC向导自动在源代码中添加注释,一般我们选择“Yes,please”。
  图10 MFC DLL的创建选项
 5.3 一个简单的MFC规则DLL
这个DLL的例子(属于静态链接到MFC 的规则DLL)中提供了一个如图11所示的对话框。
  图11 MFC规则DLL例子
 在DLL中添加对话框的方式与在MFC应用程序中是一样的。
在图11所示DLL中的对话框的Hello按钮上点击时将MessageBox一个“Hello,pconline的网友”对话框,下面是相关的文件及源代码,其中删除了MFC向导自动生成的绝大多数注释(下载本工程附件 ):
第一组文件:CWinApp继承类的声明与实现
  1. // RegularDll.h : main header file for the REGULARDLL DLL  
  2. #if !defined(AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_)  
  3. #define AFX_REGULARDLL_H__3E9CB22B_588B_4388_B778_B3416ADB79B3__INCLUDED_  
  4. #if _MSC_VER > 1000  
  5. #pragma once  
  6. #endif // _MSC_VER > 1000  
  7. #ifndef __AFXWIN_H__  
  8. #error include 'stdafx.h' before including this file for PCH  
  9. #endif  
  10. #include "resource.h" // main symbols  
  11. class CRegularDllApp : public CWinApp  
  12. {  
  13. public:  
  14. CRegularDllApp();  
  15. DECLARE_MESSAGE_MAP()  
  16. };  
  17. #endif  
  18. // RegularDll.cpp : Defines the initialization routines for the DLL.  
  19. #include "stdafx.h"  
  20. #include "RegularDll.h"  
  21. #ifdef _DEBUG  
  22. #define new DEBUG_NEW  
  23. #undef THIS_FILE  
  24. static char THIS_FILE[] = __FILE__;  
  25. #endif  
  26. BEGIN_MESSAGE_MAP(CRegularDllApp, CWinApp)  
  27. END_MESSAGE_MAP()  
  28. /////////////////////////////////////////////////////////////////////////////  
  29. // CRegularDllApp construction  
  30. CRegularDllApp::CRegularDllApp()  
  31. {  
  32. }  
  33. /////////////////////////////////////////////////////////////////////////////  
  34. // The one and only CRegularDllApp object  
  35. CRegularDllApp theApp;  

  分析:
 在这一组文件中定义了一个继承自CWinApp的类CRegularDllApp,并同时定义了其的一个实例theApp。乍一看,您会以为它是 一个MFC应用程序,因为MFC应用程序也包含这样的在工程名后添加“App”组成类名的类(并继承自CWinApp类),也定义了这个类的一个全局实例 theApp。
 我们知道,在MFC应用程序中CWinApp取代了SDK程序中WinMain的地位,SDK程序WinMain所完成的工作由CWinApp的三个函数完成:
  1. virtual BOOL InitApplication( );  
  2. virtual BOOL InitInstance( );  
  3. virtual BOOL Run( ); //传说中MFC程序的“活水源头”  

 但是MFC规则DLL并不是MFC应用程序,它所继承自CWinApp的类不包含消息循环。这是因为,MFC规则DLL不包含 CWinApp::Run机制,主消息泵仍然由应用程序拥有。如果DLL 生成无模式对话框或有自己的主框架窗口,则应用程序的主消息泵必须调用从DLL 导出的函数来调用PreTranslateMessage成员函数。
另外,MFC规则DLL与MFC 应用程序中一样,需要将所有 DLL中元素的初始化放到InitInstance 成员函数中。
第二组文件 自定义对话框类声明及实现(点击查看附件 )
分析:
 这一部分的编程与一般的应用程序根本没有什么不同,我们照样可以利用MFC类向导来自动为对话框上的控件添加事件。MFC类向导照样会生成类似ON_BN_CLICKED(IDC_HELLO_BUTTON, OnHelloButton)的消息映射宏。
第三组文件 DLL中的资源文件
  1. //{{NO_DEPENDENCIES}}  
  2. // Microsoft Developer Studio generated include file.  
  3. // Used by RegularDll.rc  
  4. //  
  5. #define IDD_DLL_DIALOG 1000  
  6. #define IDC_HELLO_BUTTON 1000  

  分析:
 在MFC规则DLL中使用资源也与在MFC应用程序中使用资源没有什么不同,我们照样可以用Visual C++的资源编辑工具进行资源的添加、删除和属性的更改。
第四组文件 MFC规则DLL接口函数
  1. #include "StdAfx.h"  
  2. #include "DllDialog.h"  
  3. extern "C" __declspec(dllexportvoid ShowDlg(void)  
  4. {  
  5. CDllDialog dllDialog;  
  6. dllDialog.DoModal();  
  7. }  

  分析:
这个接口并不使用MFC,但是在其中却可以调用MFC扩展类CdllDialog的函数,这体现了“规则”的概类。
与非MFC DLL完全相同,我们可以使用__declspec(dllexport)声明或在.def中引出的方式导出MFC规则DLL中的接口。
5.4 MFC规则DLL的调用
笔者编写了如图12的对话框MFC程序(下载本工程附件 )来调用5.3节的MFC规则DLL,在这个程序的对话框上点击“调用DLL”按钮时弹出5.3节MFC规则DLL中的对话框。
  图12 MFC规则DLL的调用例子
 下面是“调用DLL”按钮单击事件的消息处理函数:
  1. void CRegularDllCallDlg::OnCalldllButton()  
  2. {  
  3. typedef void (*lpFun)(void);  
  4. HINSTANCE hDll; //DLL句柄  
  5. hDll = LoadLibrary("RegularDll.dll");  
  6. if (NULL==hDll)  
  7. {  
  8. MessageBox("DLL加载失败");  
  9. }  
  10. lpFun addFun; //函数指针  
  11. lpFun pShowDlg = (lpFun)GetProcAddress(hDll,"ShowDlg");  
  12. if (NULL==pShowDlg)  
  13. {  
  14. MessageBox("DLL中函数寻找失败");  
  15. }  
  16. pShowDlg();  
  17. }  

  上述例子中给出的是显示调用的方式,可以看出,其调用方式与第4节中非MFC DLL的调用方式没有什么不同。
 我们照样可以在EXE程序中隐式调用MFC规则DLL,只需要将DLL工程生成的.lib文件和.dll文件拷入当前工程所在的目录,并在RegularDllCallDlg.cpp文件(图12所示对话框类的实现文件)的顶部添加:
  1. #pragma comment(lib,"RegularDll.lib")  
  2. void ShowDlg(void);  
  3.   并将void CRegularDllCallDlg::OnCalldllButton() 改为:  
  4. void CRegularDllCallDlg::OnCalldllButton()  
  5. {  
  6. ShowDlg();  
  7. }  

  5.5 共享MFC DLL的规则DLL的模块切换
 应用程序进 程本身及其调用的每个DLL模块都具有一个全局唯一的HINSTANCE句柄,它们代表了DLL或EXE模块在进程虚拟空间中的起始地址。进程本身的模块 句柄一般为0x400000,而DLL模块的缺省句柄为0x10000000。如果程序同时加载了多个DLL,则每个DLL模块都会有不同的 HINSTANCE。应用程序在加载DLL时对其进行了重定位。
 共享MFC DLL(或MFC扩展DLL)的规则DLL涉及到HINSTANCE句柄问题,HINSTANCE 句柄对于加载资源特别重要。EXE和DLL都有其自己的资源,而且这些资源的ID可能重复,应用程序需要通过资源模块的切换来找到正确的资源。如果应用程 序需要来自于DLL的资源,就应将资源模块句柄指定为DLL的模块句柄;如果需要EXE文件中包含的资源,就应将资源模块句柄指定为EXE的模块句柄。
这次我们创建一个动态链接到MFC DLL的规则DLL(下载本工程附件 ),在其中包含如图13的对话框。
  图13 DLL中的对话框
另外,在与这个DLL相同的工作区中生成一个基于对话框的MFC程序,其对话框与图12完全一样。但是在此工程中我们另外添加了一个如图14的对话框。
  图14 EXE中的对话框
图13和图14中的对话框除了caption不同(以示区别)以外,其它的都相同。
 尤其值得特别注意,在DLL和EXE中我们对图13和图14的对话框使用了相同的资源ID=2000,在DLL和EXE工程的resource.h中分别有如下的宏:
  1. //DLL中对话框的ID  
  2. #define IDD_DLL_DIALOG 2000  
  3. //EXE中对话框的ID  
  4. #define IDD_EXE_DIALOG 2000  
  5.   与5.3节静态链接MFC DLL的规则DLL相同,我们还是在规则DLL中定义接口函数ShowDlg,原型如下:  
  6. #include "StdAfx.h"  
  7. #include "SharedDll.h"  
  8. void ShowDlg(void)  
  9. {  
  10. CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框  
  11. dlg.DoModal();  
  12. }  
  13.  而为应用工程主对话框的“调用DLL”的单击事件添加如下消息处理函数:  
  14. void CSharedDllCallDlg::OnCalldllButton()  
  15. {  
  16. ShowDlg();  
  17. }  

  我们以为单击“调用DLL”会弹出如图13所示DLL中的对话框,可是可怕的事情发生了,我们看到是图14所示EXE中的对话框!
惊讶?
 产生这个问题的根源在于应用程序与MFC规则DLL共享MFC DLL(或MFC扩展DLL)的程序总是默认使用EXE的资源,我们必须进行资源模块句柄的切换,其实现方法有三:
方法一 在DLL接口函数中使用:
AFX_MANAGE_STATE(AfxGetStaticModuleState());
我们将DLL中的接口函数ShowDlg改为:
  1. void ShowDlg(void)  
  2. {  
  3. //方法1:在函数开始处变更,在函数结束时恢复  
  4. //将AFX_MANAGE_STATE(AfxGetStaticModuleState());作为接口函数的第一//条语句进行模块状态切换  
  5. AFX_MANAGE_STATE(AfxGetStaticModuleState());  
  6. CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框  
  7. dlg.DoModal();  
  8. }  

 这次我们再点击EXE程序中的“调用DLL”按钮,弹出的是DLL中的如图13的对话框!嘿嘿,弹出了正确的对话框资源。 AfxGetStaticModuleState是一个函数,其原型为:
AFX_MODULE_STATE* AFXAPI AfxGetStaticModuleState( );
 该函数的功能是在栈上(这意味着其作用域是局部的)创建一个AFX_MODULE_STATE类(模块全局数据也就是模块状态)的实例,对其进行设置,并将其指针pModuleState返回。
AFX_MODULE_STATE类的原型如下:
  1. // AFX_MODULE_STATE (global data for a module)  
  2. class AFX_MODULE_STATE : public CNoTrackObject  
  3. {  
  4. public:  
  5. #ifdef _AFXDLL  
  6. AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion);  
  7. AFX_MODULE_STATE(BOOL bDLL, WNDPROC pfnAfxWndProc, DWORD dwVersion,BOOL bSystem);  
  8. #else  
  9. AFX_MODULE_STATE(BOOL bDLL);  
  10. #endif  
  11. ~AFX_MODULE_STATE();  
  12. CWinApp* m_pCurrentWinApp;  
  13. HINSTANCE m_hCurrentInstanceHandle;  
  14. HINSTANCE m_hCurrentResourceHandle;  
  15. LPCTSTR m_lpszCurrentAppName;  
  16. … //省略后面的部分  
  17. }  

 AFX_MODULE_STATE类利用其构造函数和析构函数进行存储模块状态现场及恢复现场的工作,类似汇编中call指令对pc指针和sp寄存器的保存与恢复、中断服务程序的中断现场压栈与恢复以及操作系统线程调度的任务控制块保存与恢复。
许多看似不着边际的知识点居然有惊人的相似!
AFX_MANAGE_STATE是一个宏,其原型为:
AFX_MANAGE_STATE( AFX_MODULE_STATE* pModuleState )
该宏用于将pModuleState设置为当前的有效模块状态。当离开该宏的作用域时(也就离开了pModuleState所指向栈上对象的作用域),先前的模块状态将由AFX_MODULE_STATE的析构函数恢复。
 方法二 在DLL接口函数中使用:
AfxGetResourceHandle();
AfxSetResourceHandle(HINSTANCE xxx);
 AfxGetResourceHandle用于获取当前资源模块句柄,而AfxSetResourceHandle则用于设置程序目前要使用的资源模块句柄。
我们将DLL中的接口函数ShowDlg改为:
  1. void ShowDlg(void)  
  2. {  
  3. //方法2的状态变更  
  4. HINSTANCE save_hInstance = AfxGetResourceHandle();  
  5. AfxSetResourceHandle(theApp.m_hInstance);  
  6. CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框  
  7. dlg.DoModal();  
  8. //方法2的状态还原  
  9. AfxSetResourceHandle(save_hInstance);  
  10. }  

  通过AfxGetResourceHandle和AfxSetResourceHandle的合理变更,我们能够灵活地设置程序的资源模块句柄,而方法一则只能在DLL接口函数退出的时候才会恢复模块句柄。方法二则不同,如果将ShowDlg改为:
  1. extern CSharedDllApp theApp; //需要声明theApp外部全局变量  
  2. void ShowDlg(void)  
  3. {  
  4. //方法2的状态变更  
  5. HINSTANCE save_hInstance = AfxGetResourceHandle();  
  6. AfxSetResourceHandle(theApp.m_hInstance);  
  7. CDialog dlg(IDD_DLL_DIALOG);//打开ID为2000的对话框  
  8. dlg.DoModal();  
  9. //方法2的状态还原  
  10. AfxSetResourceHandle(save_hInstance);  
  11. //使用方法2后在此处再进行操作针对的将是应用程序的资源  
  12. CDialog dlg1(IDD_DLL_DIALOG); //打开ID为2000的对话框  
  13. dlg1.DoModal();  
  14. }  

  在应用程序主对话框的“调用DLL”按钮上点击,将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图14)。
 方法三 由应用程序自身切换
 资源模块的切换除了可以由DLL接口函数完成以外,由应用程序自身也能完成(下载本工程附件 )。
现在我们把DLL中的接口函数改为最简单的:
  1. void ShowDlg(void)  
  2. {  
  3. CDialog dlg(IDD_DLL_DIALOG); //打开ID为2000的对话框  
  4. dlg.DoModal();  
  5. }  
  6. // 而将应用程序的OnCalldllButton函数改为:  
  7. void CSharedDllCallDlg::OnCalldllButton()  
  8. {  
  9. //方法3:由应用程序本身进行状态切换  
  10. //获取EXE模块句柄  
  11. HINSTANCE exe_hInstance = GetModuleHandle(NULL);  
  12. //或者HINSTANCE exe_hInstance = AfxGetResourceHandle();  
  13. //获取DLL模块句柄  
  14. HINSTANCE dll_hInstance = GetModuleHandle("SharedDll.dll");  
  15. AfxSetResourceHandle(dll_hInstance); //切换状态  
  16. ShowDlg(); //此时显示的是DLL的对话框  
  17. AfxSetResourceHandle(exe_hInstance); //恢复状态  
  18. //资源模块恢复后再调用ShowDlg  
  19. ShowDlg(); //此时显示的是EXE的对话框  
  20. }  

  方法三中的Win32函数GetModuleHandle可以根据DLL的文件名获取DLL的模块句柄。如果需要得到EXE模块的句柄,则应调用带有Null参数的GetModuleHandle。
方法三与方法二的不同在于方法三是在应用程序中利用AfxGetResourceHandle和AfxSetResourceHandle进行资源模块 句柄切换的。同样地,在应用程序主对话框的“调用DLL”按钮上点击,也将看到两个对话框,相继为DLL中的对话框(图13)和EXE中的对话框(图 14)。





这是《VC++动态链接库(DLL)编程深入浅出》的第四部分,阅读本文前,请先阅读前三部分:(一)、(二)、(三)。
 MFC扩展DLL的内涵为MFC的扩展,用户使 用MFC扩展DLL就像使用MFC本身的DLL一样。除了可以在MFC扩展DLL的内部使用MFC以外,MFC扩展DLL与应用程序的接口部分也可以是 MFC。我们一般使用MFC扩展DLL来包含一些MFC的增强功能,譬如扩展MFC的CStatic、CButton等类使之具备更强大的能力。
 使用Visual C++向导生产MFC扩展DLL时,MFC向导会自动增加DLL的入口函数DllMain:

  1. extern "C" int APIENTRY  
  2. DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)  
  3. {  
  4. // Remove this if you use lpReserved  
  5. UNREFERENCED_PARAMETER(lpReserved);  
  6. if (dwReason == DLL_PROCESS_ATTACH)  
  7. {  
  8.   TRACE0("MFCEXPENDDLL.DLL Initializing! ");  
  9.   // Extension DLL one-time initialization  
  10.   if (!AfxInitExtensionModule(MfcexpenddllDLL, hInstance))  
  11.   return 0;  
  12.   // Insert this DLL into the resource chain  
  13.   // NOTE: If this Extension DLL is being implicitly linked to by  
  14.   // an MFC Regular DLL (such as an ActiveX Control)  
  15.   // instead of an MFC application, then you will want to  
  16.   // remove this line from DllMain and put it in a separate  
  17.   // function exported from this Extension DLL. The Regular DLL  
  18.   // that uses this Extension DLL should then explicitly call that  
  19.   // function to initialize this Extension DLL. Otherwise,  
  20.   // the CDynLinkLibrary object will not be attached to the  
  21.   // Regular DLL's resource chain, and serious problems will  
  22.   // result.  
  23.   new CDynLinkLibrary(MfcexpenddllDLL);  
  24. }  
  25. else if (dwReason == DLL_PROCESS_DETACH)  
  26. {  
  27.   TRACE0("MFCEXPENDDLL.DLL Terminating! ");  
  28.   // Terminate the library before destructors are called  
  29.   AfxTermExtensionModule(MfcexpenddllDLL);  
  30. }  
  31. return 1;  // ok  
  32. }  

  上述代码完成MFC扩展DLL的初始化和终止处理。
 由于MFC扩展DLL导出函数和变量的方式与其它DLL没有什么区别,我们不再细致讲解。下面直接给出一个MFC扩展DLL的创建及在应用程序中调用它的例子。
6.1 MFC扩展DLL的创建
 下 面我们将在MFC扩展DLL中导出一个按钮类CSXButton(扩展自MFC的CButton类),类CSXButton是一个用以取代 CButton的类,它使你能在同一个按钮上显示位图和文字,而MFC的按钮仅可显示二者之一。类CSXbutton的源代码在Internet上广泛流 传,有很好的“群众基础”,因此用这个类来讲解MFC扩展DLL有其特殊的功效。
MFC中包含一些宏,这些宏在DLL和调用DLL的应用程序中被以不同的方式展开,这使得在DLL和应用程序中,使用统一的一个宏就可以表示出输出和输入的不同意思:
  1. // for data  
  2. #ifndef AFX_DATA_EXPORT  
  3. #define AFX_DATA_EXPORT __declspec(dllexport)  
  4. #endif  
  5. #ifndef AFX_DATA_IMPORT  
  6. #define AFX_DATA_IMPORT __declspec(dllimport)  
  7. #endif  
  8. // for classes  
  9. #ifndef AFX_CLASS_EXPORT  
  10. #define AFX_CLASS_EXPORT __declspec(dllexport)  
  11. #endif  
  12. #ifndef AFX_CLASS_IMPORT  
  13. #define AFX_CLASS_IMPORT __declspec(dllimport)  
  14. #endif  
  15. // for global APIs  
  16. #ifndef AFX_API_EXPORT  
  17. #define AFX_API_EXPORT __declspec(dllexport)  
  18. #endif  
  19. #ifndef AFX_API_IMPORT  
  20. #define AFX_API_IMPORT __declspec(dllimport)  
  21. #endif  
  22. #ifndef AFX_EXT_DATA  
  23. #ifdef _AFXEXT  
  24.   #define AFX_EXT_CLASS    AFX_CLASS_EXPORT  
  25.   #define AFX_EXT_API     AFX_API_EXPORT  
  26.   #define AFX_EXT_DATA    AFX_DATA_EXPORT  
  27.   #define AFX_EXT_DATADEF  
  28. #else  
  29.   #define AFX_EXT_CLASS    AFX_CLASS_IMPORT  
  30.   #define AFX_EXT_API     AFX_API_IMPORT  
  31.   #define AFX_EXT_DATA    AFX_DATA_IMPORT  
  32.   #define AFX_EXT_DATADEF  
  33. #endif  
  34. #endif  

  导出一个类,直接在类声明头文件中使用AFX_EXT_CLASS即可,以下是导出CSXButton类的例子:
  1. #ifndef _SXBUTTON_H  
  2. #define _SXBUTTON_H  
  3. #defineSXBUTTON_CENTER-1  
  4. class AFX_EXT_CLASS CSXButton : public CButton  
  5. {  
  6. // Construction  
  7. public:  
  8. CSXButton();  
  9. // Attributes  
  10. private:  
  11. //Positioning  
  12. BOOL m_bUseOffset;    
  13. CPoint m_pointImage;  
  14. CPoint m_pointText;  
  15. int m_nImageOffsetFromBorder;  
  16. int m_nTextOffsetFromImage;  
  17. //Image  
  18. HICON m_hIcon;    
  19. HBITMAP m_hBitmap;  
  20. HBITMAP m_hBitmapDisabled;  
  21. int m_nImageWidth, m_nImageHeight;  
  22. //Color Tab  
  23. char m_bColorTab;    
  24. COLORREFm_crColorTab;  
  25. //State  
  26. BOOL m_bDefault;  
  27. UINT m_nOldAction;  
  28. UINT m_nOldState;  
  29. // Operations  
  30. public:  
  31. //Positioning  
  32. int SetImageOffset( int nPixels );  
  33. int SetTextOffset( int nPixels );  
  34. CPointSetImagePos( CPoint p );  
  35. CPointSetTextPos( CPoint p );  
  36. //Image  
  37. BOOLSetIcon( UINT nID, int nWidth, int nHeight );  
  38. BOOLSetBitmap( UINT nID, int nWidth, int nHeight );  
  39. BOOLSetMaskedBitmap( UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask );  
  40. BOOLHasImage() { return (BOOL)( m_hIcon != 0 | m_hBitmap != 0 ); }  
  41. //Color Tab  
  42. voidSetColorTab(COLORREF crTab);  
  43. //State  
  44. BOOLSetDefaultButton( BOOL bState = TRUE );  
  45. private:  
  46. BOOLSetBitmapCommon( UINT nID, int nWidth, int nHeight, COLORREF crTransparentMask, BOOL bUseMask );  
  47. voidCheckPointForCentering( CPoint &p, int nWidth, int nHeight );  
  48. voidRedraw();  
  49. // Overrides  
  50. // ClassWizard generated virtual function overrides  
  51. //{{AFX_VIRTUAL(CSXButton)  
  52. public:  
  53. virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);  
  54. //}}AFX_VIRTUAL  
  55. // Implementation  
  56. public:  
  57. virtual ~CSXButton();  
  58. // Generated message map functions  
  59. protected:  
  60. //{{AFX_MSG(CSXButton)  
  61. afx_msg LRESULT OnGetText(WPARAM wParam, LPARAM lParam);  
  62. //}}AFX_MSG  
  63. DECLARE_MESSAGE_MAP()  
  64. };  
  65. #endif  

 把SXBUTTON.CPP文件直接添加到工程,编译工程,得到“mfcexpenddll.lib”和“mfcexpenddll.dll”两 个文件。我们用Visual Studio自带的Depends工具可以查看这个.dll,发现其导出了众多符号(见图15)。
 图15 导出类时导出的大量符号 (+放大该图片)
这些都是类的构造函数、析构函数及其它成员函数和变量经编译器处理过的符号,我们直接用__declspec(dllexport)语句声明类就导出了这些符号。
 如果我们想用.lib文件导出这些符号,是非常困难的,我们需要在工程中生成.map文件,查询.map文件的符号,然后将其一一导出。如图 16,打开DLL工程的settings选项,再选择Link,勾选其中的产生MAP文件(Generate mapfile)就可以产生.map文件了。
 打开mfcexpenddll工程生成的.map文件,我们发现其中包含了图15中所示的符号(symbol)
  1. 0001:00000380 ?HasImage@CSXButton@@QAEHXZ 10001380 f i SXBUTTON.OBJ  
  2. 0001:000003d0 ??0CSXButton@@QAE@XZ    100013d0 f  SXBUTTON.OBJ  
  3. 0001:00000500 ??_GCSXButton@@UAEPAXI@Z  10001500 f i SXBUTTON.OBJ  
  4. 0001:00000570 ??_ECSXButton@@UAEPAXI@Z  10001570 f i SXBUTTON.OBJ  
  5. 0001:00000630 ??1CSXButton@@UAE@XZ    10001630 f  SXBUTTON.OBJ  
  6. 0001:00000700 ?_GetBaseMessageMap@CSXButton@@KGPBUAFX_MSGMAP@@XZ 10001700 f  SXBUTTON.OBJ  
  7. 0001:00000730 ?GetMessageMap@CSXButton@@MBEPBUAFX_MSGMAP@@XZ 10001730 f  SXBUTTON.OBJ  
  8. 0001:00000770  ?Redraw@CSXButton@@AAEXXZ 10001770 f i SXBUTTON.OBJ  
  9. 0001:000007d0  ?SetIcon@CSXButton@@QAEHIHH@Z 100017d0 f  SXBUTTON.OBJ  
  10. ……………………………………………………………………..//省略  
  图16 产生.map文件 (+放大该图片)
所以,对于MFC扩展DLL,我们不宜以.lib文件导出类。





2012年3月7日星期三

如何配置netbeans中tomcat用户名和密码

netbeans中绑定的tomcat给我们带了很多的方便,但同时也忽略了一些细节上的问题,比如如何配置tomcat管理员用户名和密码。可 netbeans的tomcat有两个conf文件夹一个实在netbeans安装文件里的,另一个则是在catalina 基目录下,在netbeans中运行环境->服务器->捆绑的tomcat->右键点击属性 就可以看到你的catalina 基目录了。 C:\Documents and Settings\

java环境变量设置 zz

windows xp下配置JDK环境变量:
      1.安装JDK,安装过程中可以自定义安装目录等信息,例如我们选择安装目录为D:/java/jdk1.5.0_08;
2.安装完成后,右击“我的电脑”,点击“属性”;
      3.选择“高级”选项卡,点击“环境变量”;
      4.在“系统变量”中,设置3项属性,JAVA_HOME,PATH,CLASSPATH(大小写无所谓),若已存在则点击“编辑”,不存在则点击“新建”;
      5.JAVA_HOME指明JDK安装路径,就是刚才安装时所选择的路径D:/java/jdk1.5.0_08,此路径下包括lib,bin,jre等文件夹(此变量最好设置,因为以后运行tomcat,eclipse等都需要依*此变量);
     
       Path使得系统可以在任何路径下识别java命令,设为:
%JAVA_HOME%/bin;%JAVA_HOME%/jre/bin
   CLASSPATH为java加载类(class or lib)路径,只有类在classpath中,java命令才能识别,设为:
.;%JAVA_HOME%/lib/dt.jar;%JAVA_HOME%/lib/tools.jar (要加.表示当前路径)
%JAVA_HOME%就是引用前面指定的JAVA_HOME;
       6.“开始”->;“运行”,键入“cmd”;
       7.键入命令“java -version”,“java”,“javac”几个命令,出现画面,说明环境变量配置成功;
       8.好了,打完收工。下面开始你的第一个java程序吧。

2012年3月5日星期一

解析Monte-Carlo算法(基本原理,理论基础,应用实践) (转载)


解析Monte-Carlo算法(基本原理,理论基础,应用实践)

2009-05-29 00:17 by T2噬菌体, 8865 visits, 收藏, 编辑

引言

      最近在和同学讨论研究Six Sigma(六西格玛)软件开发方法及CMMI相关问题时,遇到了需要使用Monte-Carlo算法模拟分布未知的多元一次概率密度分布问题。于是花了 几天时间,通过查询相关文献资料,深入研究了一下Monte-Carlo算法,并以实际应用为背景进行了一些实验。
      在研究和实验过程中,发现Monte-Carlo算法是一个非常有用的算法,在许多实际问题中,都有用武之地。目前,这个算法已经在金融学、经济学、工程 学、物理学、计算科学及计算机科学等多个领域广泛应用。而且这个算法本身并不复杂,只要掌握概率论及数理统计的基本知识,就可以学会并加以应用。由于这种 算法与传统的确定性算法在解决问题的思路方面截然不同,作为计算机科学与技术相关人员以及程序员,掌握此算法,可以开阔思维,为解决问题增加一条新的思 路。
      基于以上原因,我有了写这篇文章的打算,一是回顾总结这几天的研究和实验,加深印象,二是和朋友们分享此算法以及我的一些经验。
      这篇文章将首先从直观的角度,介绍Monte-Carlo算法,然后介绍算法基本原理及数理基础,最后将会和大家分享几个基于Monte-Carlo方法的有意思的实验。所以程序将使用C#实现。
      阅读本文需要有一些概率论、数理统计、微积分和计算复杂性的基本知识,不过不用太担心,我将尽量避免过多的数学描述,并在适当的地方对于用到的数学知识进行简要的说明。

Monte-Carlo算法引导

      首先,我们来看一个有意思的问题:在一个1平方米的正方形木板上,随意画一个圈,求这个圈的面积。
      我们知道,如果圆圈是标准的,我们可以通过测量半径r,然后用 S = pi * r^2 来求出面积。可是,我们画的圈一般是不标准的,有时还特别不规则,如下图是我画的巨难看的圆圈。

图1、不规则圆圈
      显然,这个图形不太可能有面积公式可以套用,也不太可能用解析的方法给出准确解。不过,我们可以用如下方法求这个图形的面积:
      假设我手里有一支飞镖,我将飞镖掷向木板。并且,我们假定每一次都能掷在木板上,不会偏出木板,但每一次掷在木板的什么地方,是完全随机的。即,每一次掷 飞镖,飞镖扎进木板的任何一点的概率的相等的。这样,我们投掷多次,例如100次,然后我们统计这100次中,扎入不规则图形内部的次数,假设为k,那 么,我们就可以用 k/100 * 1 近似估计不规则图形的面积,例如100次有32次掷入图形内,我们就可以估计图形的面积为0.32平方米。
      以上这个过程,就是Monte-Carlo算法直观应用例子。
      非形式化地说,Monte-Carlo算法泛指一类算法。在这些算法中,要求解的问题是某随机事件的概率或某随机变量的期望。这时,通过“实验”方法,用频率代替概率或得到随机变量的某些数字特征,以此作为问题的解。
      上述问题中,如果将“投掷一次飞镖并掷入不规则图形内部”作为事件,那么图形的面积在数学上等价于这个事件发生的概率(稍后证明),为了估计这个概率,我们用多次重复实验的方法,得到事件发生的频率 k/100 ,以此频率估计概率,从而得到问题的解。
      从上述可以看出,Monte-Carlo算法区别于确定性算法,它的解不一定是准确或正确的,其准确或正确性依赖于概率和统计,但在某些问题上,当重复实 验次数足够大时,可以从很大概率上(这个概率是可以在数学上证明的,但依赖于具体问题)确保解的准确或正确性,所以,我们可以根据具体的概率分析,设定实 验的次数,从而将误差或错误率降到一个可容忍的程度。
      上述问题中,设总面积为S,不规则图形面积为s,共投掷n次,其中掷在不规则图形内部的次数为k。根据伯努利大数定理,当试验次数增多时,k/n依概率收敛于事件的概率s/S。下面给出严格证明:
      上述证明从数学上说明用频率估计不规则图形面积的合理性,进一步可以给出误差分析,从而选择合适的实验次数n,以将误差控制在可以容忍的范围内,此处从略。
      从上面的分析可以看出,Monte-Carlo算法虽然不能保证解一定是准确和正确,但并不是“撞大运”,其正确性和准确性依赖概率论,有严格的数学基础,并且通过数学分析手段对实验加以控制,可以将误差和错误率降至可容忍范围。

Monte-Carlo算法的数理基础

      这一节讨论Monte-Carlo算法的数理基础。
      首先给出三个定义:优势,一致,偏真。这三个定义在后面会经常用到。
      1) 设p为一个实数,且0.5<p<1。如果一个Monte-Carlo方法对问题任一实例的得到正确解的概率不小于p,则该算法是p正确的,且p-0.5叫做此算法的优势。
      2) 如果对于同一实例,某Monte-Carlo算法不会给出不同的解,则认为该算法时一致的。
      3) 如果某个解判定问题的Monte-Carlo算法,当返回true时是一定正确的。则这个算法时偏真的。注意,这里没有定义“偏假”,因为“偏假”和偏真是等价的。因为只要互换算法返回的true和false,“偏假”就变成偏真了。
      下面,我们讨论Monte-Carlo算法的可靠性和误差分析。
      总体来说,适用于Monte-Carlo算法的问题,比较常见的有两类。一类是问题的解等价于某事件概率,如上述求不规则图形面积的问题;另一类是判定问题,即判定某个命题是否为真,如主元素存在性判定和素数测试问题。
      先来分析第一类。对于这类问题,通常的方法是通过大量重复性实验,用事件发生的频率估计概率。之所以能这样做的数学基础,是伯努利大数法则:事件发生的频 率依概率收敛于事件的概率p。这个法则从数学生严格描述了频率的稳定性,直观意义就是当实验次数很大时,频率与概率偏差很大的概率非常小。此类问题的误差 分析比较繁杂,此处从略。有兴趣的朋友可以参考相关资料。
      接着,我们分析第二类问题。这里,我们只关心一致且偏真的判定问题。下面给出这类问题的正确率分析:
      由以上分析可以看到,对于一致偏真的Monte-Carlo算法,即使调用一次得到正确解的概率非常小,通过多次调用,其正确率会迅速提高,得到的结果非 常可靠。例如,对一个q为0.5的问题,假设p仅为0.01,通过调用1000次,其正确率约为0.9999784,几乎可以认为是绝对准确的。重要的 是,使用Monte-Carlo算法解判定问题,其正确率不随问题规模而改变,这就使得仅需要损失微乎其微的正确性,就可以将算法复杂度降低一个数量级, 在后面中可以看到具体的例子。

应用实例一:使用Monte-Carlo算法计算定积分

      计算定积分是金融、经济、工程等领域实践中经常遇到的问题。通常,计算定积分的经典方法是使用Newton-Leibniz公式:
      这个公式虽然能方便计算出定积分的精确值,但是有一个局限就是要首先通过不定积分得到被积函数的原函数。有的时候,求原函数是非常困难的,而有的函数,如 f(x) = (sinx)/x,已经被证明不存在初等原函数,这样,就无法用Newton-Leibniz公式,只能另想办法。
      下面就以f(x) = (sinx)/x为例介绍使用Monte-Carlo算法计算定积分的方法。首先需要声明,f(x) = (sinx)/x在整个实数域是可积的,但不连续,在x = 0这一点没有定义。但是,当x趋近于0其左右极限都是1。为了严格起见,我们补充定义当x = 0时f(x) = 1。另外为了需要,这里不加证明地给出f(x)的一些性质:补充x = 0定义后,f(x)在负无穷到正无穷上连续、可积,并且有界,其界为1,即|f(x)| <= 1,当且仅当x = 0时f(x) = 1。
      下面开始介绍Monte-Carlo积分法。为了便于比较,在本节我们除了介绍使用Monte-Carlo方法计算定积分外,同时也探讨和实现数值计算中常用的插值积分法,并通过实验结果数据对两者的效率和精确性进行比较。

1、插值积分法

      我们知道,对于连续可积函数,定积分的直观意义就是函数曲线与x轴围成的图形中,y>0的面积减掉y<0的面积。那么一种直观的数值积分方法 是通过插值方法,其中最简单的是梯形法则:用以f(a)和f(b)为底,x轴和f(a)、f(b)连线为腰组成的梯形面积来近似估计积分。如下图所示。

图2、梯形插值
      如图2所示,蓝色部分是x1到x2积分的精确面积,而在梯形插值中,用橙色框所示的梯形面积近似估计积分值。
      显然,梯形法则的效果一般,而且某些情况下偏差很大,于是,有人提出了一种改进的方法:首先将积分区间分段,然后对每段计算梯形插值再加起来,这样精度就大大提高了。并且分段越多,精度越高。这就是复化梯形法则。
      除了梯形插值外,还有许多插值积分法,比较常见的有Sinpson法则,当然对应的也有复化Sinpson法则。下面给出四种插值积分的公式:
      下面是四种插值积分法的程序代码,用C#编写。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
  
namespace MonteCarlo.Integration
{
    /// <summary>
    /// 数值法求积分
    /// 被积函数为 f(x) = (sin x)/x
    /// </summary>
    public class NumericalIntegrator
    {
        /// <summary>
        /// 梯形法则求积分
        /// 积分公式为:((b - a) / 2) * [f(a) + f(b)]
        /// </summary>
        /// <param name="a">积分下限</param>
        /// <param name="b">积分上限</param>
        /// <returns>积分值</returns>
        public static double TrapezoidalIntegrate(double a, double b)
        {
            return ((b - a) / 2) * (Math.Sin(a) / a + Math.Sin(b) / b);
        }
  
        /// <summary>
        /// 复化梯形法则求积分
        /// 积分公式为:累加((xi - xi-1) / 2) * [f(xi) + f(xi-1)]  (i=1,2,...,n)
        /// </summary>
        /// <param name="a">积分下限</param>
        /// <param name="b">积分上限</param>
        /// <param name="n">分段数量</param>
        /// <returns>积分值</returns>
        public static double ComplexTrapezoidalIntegrate(double a, double b, int n)
        {
            double result = 0;
            for (int i = 0; i < n; i++)
            {
                double xa = a + i * (b - a) / n;//区间积分下限
                double xb = xa + (b - a) / n;//区间积分上限
  
                result += ((xb - xa) / 2) * (Math.Sin(xa) / xa + Math.Sin(xb) / xb);
            }
  
            return result;
        }
  
        /// <summary>
        /// Sinpson法则求积分
        /// 积分公式为:((b - a) / 6) * [f(a) + 4 * f((a + b) / 2) + f(b)]
        /// </summary>
        /// <param name="a">积分下限</param>
        /// <param name="b">积分上限</param>
        /// <returns>积分值</returns>
        public static double SinpsonIntegrate(double a, double b)
        {
            return ((b - a) / 6) * (Math.Sin(a) / a + 4 * (Math.Sin(a + b) / (2 * (a + b))) + Math.Sin(b) / b);
        }
  
        /// <summary>
        /// 复化Sinpson法则求积分
        /// 积分公式为:累加(h / 3) * [f(x2i-2) + 4*(f(x2i-1)) + f(x2i)]  (i=1,2,...,n/2 h = (b - a) / n)
        /// </summary>
        /// <param name="a">积分下限</param>
        /// <param name="b">积分上限</param>
        /// <param name="n">分段数量(必须为偶数)</param>
        /// <returns>积分值</returns>
        public static double ComplexSinpsonIntegrate(double a, double b, int n)
        {
            double result = 0;
            for (int i = 0; i < n / 2 - 1; i++)
            {
                double xa = a + 2 * i * (b - a) / n;//区间积分下限
                double xb = xa + (b - a) / n;//区间积分限中点
                double xc = xb + (b - a) / n;//区间积分上限
                result += ((b - a) / (3 * n) * (Math.Sin(xa) / xa + 4 * (Math.Sin(xb) / xb) + Math.Sin(xc) / xc));
            }
  
            return result;
        }
    }
}
2、Monte-Carlo积分法
      我们知道,求定积分的直观意义就是求面积,所以,用Monte-Carlo求积分的原理就是通过模拟统计方法求解面积。即通过向特定区域随机产生大量点, 然后统计点落在函数区域内的频率,以此频率估计面积,从而得到积分值。下面给出Monte-Carlo求取积分的算法程序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
  
namespace MonteCarlo.Integration
{
    /// <summary>
    /// Monte-Carlo法求积分
    /// 被积函数为 f(x) = (sin x)/x
    /// </summary>
    public class MonteCarloIntegrator
    {
        /// <summary>
        /// 用Monte-Carlo法求解积分值
        /// </summary>
        /// <param name="a">积分下限</param>
        /// <param name="b">积分上限</param>
        /// <param name="N">模拟次数</param>
        /// <returns>积分值</returns>
        public static double MonteCarloIntegrate(int a, int b, int N)
        {
            Random random = new Random();
            int positivePointCount = 0;//y >=0 区间内落入函数曲线内的点数目
            int negativePointCount = 0;//y < 0区间内落入函数曲线内的点数目
  
            //统计y >= 0区间点分布
            for (int i = 0; i < N; i++)
            {
                double xCoordinate = random.NextDouble();//随机产生的x坐标
                double yCoordinate = random.NextDouble();//随机产生的y坐标
                xCoordinate = a + (b - a) * xCoordinate;//将x规格化到相应积分区间
                //yCoordinate = 1 * yCoordinate;//将y规格化到相应区间
                if (Math.Sin(xCoordinate) / xCoordinate >= yCoordinate)
                {
                    positivePointCount++;
                }
            }
  
            //统计y < 0区间点分布
            for (int i = 0; i < N; i++)
            {
                double xCoordinate = random.NextDouble();//随机产生的x坐标
                double yCoordinate = random.NextDouble();//随机产生的y坐标
                xCoordinate = a + (b - a) * xCoordinate;//将x规格化到相应积分区间
                yCoordinate = -1 * yCoordinate;//将y规格化到相应区间
                if (Math.Sin(xCoordinate) / xCoordinate <= yCoordinate)
                {
                    negativePointCount++;
                }
            }
  
            double positiveFrequency = (double)positivePointCount / (double)N;//y >= 0区间内函数内点频率
            double negativeFrequency = (double)negativePointCount / (double)N;//y < 0区间内函数内点频率
  
            return (positiveFrequency - negativeFrequency) * (double)(b - a);
        }
    }
}
3、积分法的测试与比较
      下面对各种积分方法进行测试,对sinx/x在[1,2]区间上进行定积分。其中,我们分别对复化梯形和复化Sinpson法则做分段为 10,10000,和10000000的积分测试。另外,对Monte-Carlo法也投点数也分为10,10000,和10000000。测试结果如 下:

图3、积分法测试结果
      为了分析偏差,我们必须给出一个精确值。但是现在我手头没有这个积分的精确值,不过1000万次的梯形法则和Sinpson法则已经精确度很高了,所以这里就以0.65932985作为基本,进行误差分析。下面给出分析结果:

表1、积分方法实验结果
      首先看时间效率。当频度较低时,各种方法没有太多差别,但在1000万级别上复化梯形和复化Sinpson相差不大,而Monte-Carlo算法的效率快一倍。
      而从准确率分析,当频度较低时,几种方法的误差都很大,而随着频度提高,插值法要远远优于Monte-Carlo算法,特别在1000万级别 时,Monte-Carlo法的相对误差是插值法的的近万倍。总体来说,在数值积分方面,Monte-Carlo方法效率高,但准确率不如插值法。

应用实例二:在O(n)复杂度内判定主元素

      这次,我们看一个判定问题。问题是这样的:在一个长度为n的数组中,如果有超过[n/2]的元素具有相同的值,那么具有这个值的元素叫做数组的主元素。现在要求给出一种算法,在O(n)时间内判定给定数组是否存在主元素。
      如果采用确定性算法,由于最坏情况下要搜索n/2次,而每次要比较的次数为O(n)量级,这样,算法的复杂度就是O(n^2),不可能在O(n)时间内完 成。所以我们只好换一种思路:不是要一个一定正确的结果,而只需要结果在很大概率上正确就行。我们可以这样做:

图4、Monte-Carlo法判定主元素
      上述算法,就是用Monte-Carlo思想求解主元素判定问题的过程。由于阈值N是一个给定的常数,不随规模变化而变化,所以这个算法的时间复杂度为 O(n),符合题设要求。但这个算法给出的解并不是100%正确的,正确率和N有关。N设得过大,影响效率,N太小,正确率太低,那么到底N设多大合适 呢。这就要对算法进行概率分析。
      首先,这个算法是一致且偏真的,证明很简单,这里从略。所以,如果数组中不存在主元素,则结果一定正确,而如果存在,调用一次得到正确结果的概率不低于 1/2。由于偏真,在N次调用中只要返回一次True,就可以认为得到正确结果,所以,调用N此得到正确结果的概率不低于1 – (1/2)^N,可以看到,随着N的增大,这个概率增加很快,只要调用10次,正确率就可以达到99.9%,重要的是,这个正确率和规模无关,即使数组的 元素有1千万亿,只需调用10次,正确率依然是99.9%,这就体现出在数组很大时,Monte-Carlo方法的优势。
      下面是使用Monte-Carlo算法进行主元素测试的C#程序示例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
  
namespace MonteCarlo.Detection
{
    public class PrincipalElementDetector
    {
        /// <summary>
        /// 使用Monte-Carlo发探测主元素
        /// </summary>
        /// <param name="elements">所有元素</param>
        /// <param name="N">阈值</param>
        /// <returns>是否存在主元素</returns>
        public static bool DetectPrincipalElement(IList<int> elements,int N)
        {
            Random random = new Random();
            bool result = false;
            for (int i = 0; i <= N; i++)
            {
                int index = random.Next(0, elements.Count - 1);
                int element = elements[index];
                int count = 0;
                for (int j = 0; j < elements.Count; j++)
                {
                    if (element == elements[j])
                    {
                        count++;
                    }
                }
                if (count >= elements.Count / 2)
                {
                    result = true;
                    break;
                }
            }
  
            return result;
        }
    }
}
      程序很简单,不做赘述。下面测试这个算法。我们分别将阈值设为1、3、10,并且在每个阈值下测试100次,看看这个算法的准确率如何。测试数组是[ 4, 5, 8, 1, 8, 4, 9, 2, 2, 2, 2, 2, 5, 7, 8, 2, 2, 2, 2, 2, 1, 0, 9, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 4, 7, 8, 2, 2, 2, 2, 2, 0, 1, 2, 2, 2, 2, 2 ],其中存在主元素2。下面是测试结果:

图5、Monte-Carlo算法判定主元素实验结果
      测试数组有49个元素,主元素2有29个,比率为59%。从测试结果可以看出,即使阈值为1,正确率也高达84%,而仅仅为3的阈值就使正确率升高到 98%,阈值为10时,100次测试全部正确。虽然理论上来说,阈值为10时有0.41^10=0.013%的概率给出错误判断,但是笔者多次试验,还没 有在阈值为10时得到错误结果。所以,Monte-Carlo方法求解判定问题,不论从理论上还是实践中,都是不错的方法。
      另外一个与判定主元素类似的应用是素数判定问题,我们知道,对于寻找上百位的大素数,完全测试在时间效率上时不允许的。于是,结合费马小定理使用Monte-Carlo法进行素数判定,是广泛使用的方法。具体这里不再详述,感兴趣的朋友可以参考相关资料。

应用实例三:分布未知的概率密度函数模拟

      现在我们来看看Monte-Carlo算法的第三种应用:模拟。在这种应用中,不再是用Monte-Carlo算法求解问题,而是用来模拟难以解析描述的东西。问题是这样的:
11
      这个问题是实验室一个师兄在开发Six Sigma软件开发过程管理工具时遇到的一个实际需求,最终Y的概率密度函数将被用来计算分位点,从而进行过程控制。其中X可能是正态分布(高斯分布)、 泊松分布、均匀分布或指数分布等。将多个不同分布的概率密度函数相加,得到的Y的分布式很难解析表示出来的,但如果是为了计算分位点,我们可以采取这样一 个策略:对于每一个X,产生若干符合其分布的点,带入公式就得到若干符合Y分布的点,然后分段计算频率,从而模拟出Y的分布,这些模拟点也可以用于分位点 计算。这就是Monte-Carlo模拟的思想。
      下面我们实现这个算法,这里的X我们仅给出最常用的正态分布,如果要实现其他分布,只要编写相应的随机点发生器就可以了。由于C#中只能产生符合均匀分布 的随机数,所以我们需要一种算法,将均匀分布的随机数转为正态分布随机数。这种算法很多,Marc Brysbaert在1991年发表的《Algorithms for randomness in the behavioral sciences: A tutorial》一文中,共总结了5种将均匀分布随机数转为正态分布的随机数的算法,这里笔者用到的是Knuth在1981年提出的一种算法。这个算法 是将符合u(0,1)均匀分布的随机点转换为符合N(0,1)标准正态分布的随机点p,由概率知识可知,要转为符合N(e,v)的一般正态分布,只需进行 p*v+e即可。下面是这个算法:
      下面是根据这个算法,使用C#编写的正态分布随机点发生器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
  
namespace MonteCarlo.DistributingSimulation
{
    public class NormalDistributingGenerator
    {
        /// <summary>
        /// 产生符合正态分布的随机数
        /// 正态分布的期望为expectation,方差为variance
        /// </summary>
        /// <param name="expectation">期望</param>
        /// <param name="variance">方差</param>
        /// <param name="N">产生的数量</param>
        /// <returns>随机数序列</returns>
        public static IList<double> GenerateNDRNumber(double expectation, double variance, int N)
        {
            Random random = new Random();
            IList<double> randomList = new List<double>();
            for (int i = 0; i < N; i++)
            {
                double u1, u2, v, z, a;
                do
                {
                    u1 = random.NextDouble();
                    u2 = random.NextDouble();
                    v = 0.8578 * (2 * u2 - 1);
                    z = v / u1;
                    a = 0.25 * Math.Exp(2);
  
                    if (a < 1 - u1)
                    {
                        break;
                    }
  
                } while (a > 0.295 / u1 + 0.35 || a > -Math.Log(u1, Math.E));
  
                randomList.Add(z * Math.Sqrt(variance) + expectation);
            }
  
            return randomList;
        }
    }
}
      接着是利用这个正态分布发生器获得X的随机值,并计算出Y的随机值的代码。也就是Y的随机点发生器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
  
namespace MonteCarlo.DistributingSimulation
{
    public class DistributingSimulator
    {
        /// <summary>
        /// 模拟多个正态分布之和的分布情况,产生符合复合分布的随机点
        /// y = a0 + a1*N(e1,v1) + ... + an*N(en,vn)
        /// N(e,v)表示期望为e,方差为v的正态分布
        /// </summary>
        /// <param name="a">常数列</param>
        /// <param name="e">期望列</param>
        /// <param name="v">方差列</param>
        /// <param name="N">产生模拟点的个数</param>
        /// <returns>模拟点序列</returns>
        public static IList<double> Simulate(IList<double> a,IList<double> e,IList<double> v,int N)
        {
            IList<double> result = new List<double>();
            IList<IList<double>> randomLists = new List<IList<double>>();
            int count = a.Count - 1;
  
            //产生各个自变量的随机序列
            for (int i = 1; i <= count; i++)
            {
                randomLists.Add(NormalDistributingGenerator.GenerateNDRNumber(e[i], v[i], N));
            }
  
            //带入公式
            for (int j = 0; j < N; j++)
            {
                double y = 0;
                for (int k = 1; k <= count; k++)
                {
                    y += a[k] * randomLists[k - 1][j];
                }
                y += a[0];
                result.Add(y);
            }
  
            return result;
        }
    }
}
      这样,我们就可以产生任意多个符合Y分布的随机点,从而借此模拟Y的概率密度分布。
      接着,我们测试一下这个模拟程序的效果,首先我们将初始值设为仅有一个符合标准正态分布的X,这样Y=X,我们看看直接模拟一个标准正态分布的效果。这里,我们产生100万个随机点。

图6、使用Monte-Carlo算法模拟标准正态分布
      可以看到,模拟效果基本令人满意。接下来,我们实际应用这个程序模拟一个分布未知的Y,其中Y = 15 + 2*N(2,8) + 5*N(-10,9) + 7*N(0,0.5)。模拟结果如下:

图7、使用Monte-Carlo算法模拟未知分布
      有了符合Y分布的大量随机点以及频率统计,就可以随心所欲绘出分布模拟图,并进行分位点计算。这样就用Monte-Carlo算法解决了本节开头提到的问题。

总结

      本文首先通过一个不规则图形面积计算的例子直观介绍了Monte-Carlo算法,然后给出了Monte-Carlo算法在应用过程中需要了解的数理基础。然后大篇幅介绍了三个应用:计算、判定和模拟。
      总体来说,当需要求解的问题依赖概率时,Monte-Carlo方法是一个不错的选择。但这个算法毕竟不是确定性算法,在应用过程中需要冒一定“风险”, 这就要求不能滥用这个算法,在应用过程中,需要对其准确率或正确率进行数理分析,合理设计实验,从而得到良好的结果,并将风险控制在可容忍的范围内。
      其实,不确定性算法不只Monte-Carlo一种,Sherwood算法、Las Vegas算法和遗传算法等也是经典的不确定算法。在很多问题上,不确定性算法具有很好大的应用价值。有兴趣的朋友可以参考相关资料。
参考文献
      [1] 孙海燕,周梦等 著,应用数理统计。北京航空航天大学出版社,2008.8
      [2] 盛骤,谢式千,潘承毅 著,概率论与数理统计。高等教育出版社,2006.12
      [3] David Kincaid,WardCheney 著,王国荣等 译,数值分析(原书第三版)。机械工业出版社,2005.9
      [4] Thomas H. Cormen等 著,算法导论(第二版,影印版)。高等教育出版社,2002.5
      [5] 王晓东 著,计算机算法设计与分析。电子工业出版社,2001.1
      [6] Marc Brysbaert,Algorithms for randomness in the behavioral sciences: A tutorial。Behavior Research Methods, Instruments, & Computers 1991, 23 (1) 45-60
      [7] Patrick Smacchia 著,施凡等 译,C#和.NET2.0 平台、语言与框架。2008.1
      [8] Google。www.google.com
      [9] Wikipedia。www.wikipedia.org