第九讲 创建用户模块
这一讲介绍Visual C++的用户模块的概念,并介绍静态库、动态连接库、扩展类库的设计与使用。
9.1 用户模块
用户模块是由用户自己开发的、可以加入到最终用户(包括用户本人和其他使用该模块的人)应用程序中提供某一特定功能的函数和类的集合。
为了完成同样的工作,也可以向最终用户提供源程序。但是,使用用户模块有许多好处:首先是省去用户管理源代码的烦恼,用户许多情况下往往并不关心模块的内部实现,他只是想把它作为一个黑匣子使用。另外,模块的开发者有时候并不希望模块使用者看到源代码。还有,使用模块而不使用源代码还可以避免模块的函数名、变量名与最终用户的程序上的冲突。
用户模块可分为两大类:静态连接库和动态连接库。
静态连接库提供了函数的完整的目标代码,如果程序调用静态连接库中的函数,则在进行连接时连接程序将静态连接库中所包含的该函数的代码拷贝至运行文件中。
动态连接库是一个可执行模块,其包含的函数可以由Windows应用程序调用以执行一些功能。动态连接库主要为应用程序模块提供服务。Windows内核的三个模块USER.EXE、KENERL.EXE和GDI.EXE实际上都是动态连接库,分别提供用户消息服务、进程管理、图形输出等服务。
动态连接库也包含了其所提供的函数的目标代码,但是在程序连接动态连接库中的函数时,连接程序并不将包含在动态连接库中的函数的目标代码拷贝至运行文件,而只是简单地记录了函数的位置信息(即包含于哪个动态连接库中以及在动态连接库中的位置)。有了这些信息后,程序在执行时,即可找到该函数的目标代码。因为只是在执行时才得到真正的连接,因此称为动态连接。提供函数在动态连接库中位置的信息存放在一个独立的文件中,这个文件就是引入库(IMPORT LIB)。
由于静态连接库将目标代码连接到应用程序中,当程序运行时,如果两个程序调用了同一静态库中的函数,内存中将出现该函数的多份拷贝。而动态连接库则更适合于多任务环境:当两个应用程序调用了同一动态连接库中的同一个函数时,内存中只保留该函数的一份拷贝,这样内存利用率更高。
利用动态连接库还可以实现资源共享:像Windows下的串行口、并行口驱动程序都是动态连接库;另外,Windows下的字体也是动态连接库。
但是,静态库由于将目标代码连入应用程序中,应用程序可独立运行。而使用动态连接库时,随同应用程序还要提供动态连接库文件(DLL文件)。比如,发布Visual C++编写的程序时,如果使用了动态连接,则在提供可执行文件同时还需要提供Visual C++的动态连接库。
应用程序和动态连接库都是完成一定功能的可执行模块。它们的区别是:应用程序有自己的消息循环,而动态连接库没有自己的消息循环(但是它可以发送消息);应用程序一般是主动完成某一功能的,而动态连接库主要是被动(在中断驱动程序中也主动完成一些功能)的提供服务。
9.2静态连接库
9.2.1创建静态库
现在以一个简单的数学函数库为例介绍静态库的创建和使用。
要创建静态库,选择File->New菜单,弹出New对话框。选择Projects标签,在项目类型列表框中选择Win32 Static Library,在Name中输入mymath,表明要创建一个mymath.lib的静态库文件。
然后用Project->Add to Project->Files菜单往mymath工程中加入以下两个文件:
1.头文件(见清单9.1):定义了Summary和Factorial两个函数,分别用于完成求和与阶乘。注意这里使用C风格的函数,需要加入extern “C”关键字,表明它是C风格的外部函数。
清单9.1 头文件
#ifndef _MYMATH_H
#define _MYMATH_H
extern “C”
{
int Summary(int n);
int Factorial(int n);
}
#endif
2.源文件:包含了Summary和Factorial函数的定义,见清单9.2。
清单9.2 源文件
int Summary(int n)
{
int sum=0;
int i;
for(i=1;i<=n;i++)
{
sum+=i;
}
return sum;
}
int Factorial(int n)
{
int Fact=1;
int i;
for(i=1;i<=n;i++)
{
Fact=Fact*i;
}
return Fact;
}
在Build菜单下,选择Build菜单下的Build mymath.lib。Visual C++编译链接工程,在mymath/debug目录下生成mymath.lib文件。至此,静态连接库生成的工作就做完了。下面用一个小程序来测试这个静态库。
提示:用户在交付最终静态连接库时,只需要提供.lib文件和头文件,不需要再提供库的源代码。
9.2.2测试静态库
用AppWizard生成一个基于对话框的应用程序test。打开test资源文件,修改IDD_TEST_DIALOG对话框资源,加入两个按钮。按钮ID和文字为:
IDC_SUM “&Summary”
IDC_FACTORIAL “&Factorial”
如图9-1所示。
图9-1 修改test对话框
用ClassWizard为上述两个按钮Click事件生成消息处理函数OnSum和OnFactorial,并加入代码,修改后的OnSum和OnFactorial见清单9.3。
清单9.3 OnSum和OnFactorial函数定义
void CTestDlg::OnSum()
{
// TODO: Add your control notification handler code here
int nSum=Summary(10);
CString sResult;
sResult.Format("Sum(10)=%d",nSum);
AfxMessageBox(sResult);
}
void CTestDlg::OnFactorial()
{
// TODO: Add your control notification handler code here
int nFact=Factorial(10);
CString sResult;
sResult.Format("10!=%d",nFact);
AfxMessageBox(sResult);
}
由于要使用mymath.lib中的函数,首先要将mymath.lib和mymath.h两个文件拷贝到test目录下。然后用Project->Add to Project->Files命令,将mymath.lib加入到工程中。
在testdlg.cpp文件头部,还要加入头文件mymath.h:
#include "stdafx.h"
#include "Test.h"
#include "TestDlg.h"
#include "mymath.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
编译运行test程序,点Factorial按钮,弹出如图9-2的消息框。
图9-2 Test程序运行结果
9.3创建动态连接库
在一些情况下,必须使用动态连接库:
1.多个应用程序共享代码和数据:比如Office软件的各个组成部分有相似的外观和功能,这就是通过共享动态连接库实现的。
2.在钩子程序过滤系统消息时必须使用动态连接库
3.设备驱动程序必须是动态连接库
4.如果要在对话框编辑器中使用自己定义的控件,也必须使用动态连接库
5.动态连接库以一种自然的方式将一个大的应用程序划分为几个小的模块,有利于小组内部成员的分工与合作。而且,各个模块可以独立升级。如果小组中的一个成员开发了一组实用例程,他就可以把这些例程放在一个动态连接库中,让小组的其他成员使用。
6.为了实现应用程序的国际化,往往需要使用动态连接库。使用动态连接库可以将针对某一国家、语言的信息存放在其中。对于不同的版本,使用不同的动态连接库。在使用AppWizard生成应用程序时,我们可以指定资源文件使用的语言,这就是通过提供不同的动态连接库实现的。
MFC支持两类动态连接库的创建:
用户动态连接库
MFC扩展类库。
9.3.1用户动态连接库(_USRDLL)
用户动态连接库一般使用C语言接口。要创建一个动态连接库,选择File->New菜单,弹出New对话框。在Projects标签页下,选择“Win32 Dynamic-Link Library”。Visual C++就会创建动态连接库所需的工程文件和MAK文件。
然后把下面两个文件加入到工程中(Project-Add to Project-Files菜单)。
文件1:mymaths.cpp
//mymaths.cpp
//
//a maths API DLL.
//
///
#include<windows.h>
//Declare the DLL functions prototypes
int Summary(int);
int Factorial(int);
//
//DllEntryPoint():The entry point of the DLL
//
/
BOOL WINAPI DLLEntryPoint(HINSTANCE hDLL,DWORD dwReason,
LPVOID Reserved)
{
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
{
//一些初始化代码
break;
}
case DLL_PROCESS_DETACH:
{
//一些用于清理的代码
break;
}
}
return TRUE;
}
int Summary(int n)
{
int sum=0;
int i;
for(i=1;i<=n;i++)
{
sum+=i;
}
return sum;
}
int Factorial(int n)
{
int Fact=1;
int i;
for(i=1;i<=n;i++)
{
Fact=Fact*i;
}
return Fact;
}
文件2:mymaths.def
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;Mymaths.DEF
;
;The DEF file for the Mymaths.DLL DLL.
;
LIBRARY mymaths
CODE PRELOAD MOVEABLE DISCARDABLE
DATA PRELOAD SINGLE
EXPORTS
;The names of the DLL functions
Summary
Factorial
在文件mymaths.cpp开头,声明了动态连接库所包含的两个函数:Summary和Factorial。接着是DllEntryPoint()函数的定义。DllEntryPoint()顾名思义是动态连接库的入口,应用程序通过该入口访问动态连接库提供的服务。DllEntryPoint()主体是一个switch/case语句:
switch(dwReason)
{
case DLL_PROCESS_ATTACH:
{
//一些初始化代码
break;
}
case DLL_PROCESS_DETACH:
{
//一些用于清理的代码
break;
}
}
其中,在case DLL_PROCESS_ATTACH分支可加入动态连接库执行时的一些初始化代码。在case DLL_PROCESS_DETACH加入动态连接库被卸载时的一些清理代码,比如释放动态连接库运行时申请的内存等。
在DllEntryPoint()函数后,是两个函数Summary和Factorial函数的定义。它们的定义与前面的静态库完全相同。在这里用户可以放入任何函数。
另外,我们还需要一个mymaths.def文件。这个文件记录了可被外部应用程序使用的DLL库函数名字。这些名字信息和对应的函数位置的信息将被编译进动态连接库文件中,然后应用程序根据函数名字和函数位置对照表来找到对应的函数。
按F7编译工程,Visual C++就在mymaths/debug目录下生成一个mymaths.dll动态连接库文件。
现在,我们来使用刚才生成的动态连接库。我们并不重新生成一个程序,而是修改前面测试静态库时的test程序。首先,把mymaths/debug目录下的mymaths.dll拷贝到test/debug目录下。test程序运行时,会在该目录下搜索动态连接库文件。然后修改testdlg.h,在其中加入一个函数LoadDLL()的声明,见清单9.4。LoadDLL用于载入动态连接库。
清单9.4 修改后的对话框头文件
class CTestDlg : public CDialog
{
// Construction
public:
CTestDlg(CWnd* pParent = NULL); // standard constructor
protected:
void LoadDLL();
//......
}
然后修改testdlg.cpp,修改后如清单9.5。
清单95. TestDlg.cpp文件
// TestDlg.cpp : implementation file
//
#include "stdafx.h"
#include "Test.h"
#include "TestDlg.h"
//#include "mymath.h" //注释掉mymath.h头文件
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
//The instance of the Mymaths.DLL library
HINSTANCE ghMathsDLL=NULL;
//declare the Summary() function from the Mymaths.DLL libray.
typedef int (*SUMMARY)(int);
SUMMARY Summary;
//declare the Factorial() function from
//the Mymaths.DLL library.
typedef int (*FACTORIAL)(int);
FACTORIAL Factorial;
/
// CAboutDlg dialog used for App About
class CAboutDlg : public CDialog
{
//...
};
//CAboutDlg的一些成员函数定义
//CTestDlg的一些成员函数定义
void CTestDlg::OnSum()
{
// TODO: Add your control notification handler code here
LoadDLL();
int nSum=Summary(10);
CString sResult;
sResult.Format("Sum(10)=%d",nSum);
AfxMessageBox(sResult);
}
void CTestDlg::OnFactorial()
{
// TODO: Add your control notification handler code here
LoadDLL();
int nFact=Factorial(10);
CString sResult;
sResult.Format("10!=%d",nFact);
AfxMessageBox(sResult);
}
void CTestDlg::LoadDLL()
{
//如果DLL已经载入,则返回
if(ghMathsDLL!=NULL)
{
return;
}
//载入Mymaths.DLL文件.
ghMathsDLL=LoadLibrary("mymaths.DLL");
//如果载入DLL失败,提示用户
if(ghMathsDLL==NULL)
{
AfxMessageBox("Cannot load DLL file!");
}
//获得DLL中Summary函数的地址
Summary=(SUMMARY)GetProcAddress(ghMathsDLL,"Summary");
//获得DLL中Factorial函数的地址
Factorial=(FACTORIAL)GetProcAddress(ghMathsDLL,"Factorial");
}
在testdlg.cpp文件开头,加入:
//The instance of the Mymaths.DLL library
HINSTANCE ghMathsDLL=NULL;
//declare the Summary() function from the Mymaths.DLL libray.
typedef int (*SUMMARY)(int);
SUMMARY Summary;
//declare the Factorial() function from
//the Mymaths.DLL library.
typedef int (*FACTORIAL)(int);
FACTORIAL Factorial;
首先加入一个ghMathsDLL的全局变量,它是动态连接库载入后的句柄(同应用程序一样,每个动态连接库载入都会有一个句柄和它相对应)。应用程序通过句柄访问库中的函数。然后加入Summary和Factorial函数指针的类型定义。
在LoadDLL()函数定义中,检查动态连接库句柄是否为空;若为空,则用LoadLibrary载入该动态连接库。然后用GetProcAddress取得Summary和Factorial函数地址。
在OnFactorial和OnSummary函数开头,调用LoadDLL(),载入动态连接库。现在编译运行程序,按Factorial按钮测试一下程序。
应用程序是如何查找DLL文件的
应用程序test按以下顺序查找动态连接库文件:
- 当前目录下(因此要将动态连接库拷贝至DEBUG目录下,因为可执行文件在该目录下)
- Windows目录
- Windows系统目录
- PATH环境变量中设置的目录
- 列入映射网络的目录表中的目录
调用动态连接库中的函数的方法
有两种方法可以调用动态连接库中的函数:
1.通过引入库:
利用Visual C++提供的IMPLIB工具为动态连接库生成引入库,为引入库设计一个头文件:
#ifndef _MYMATH_H
#define _MYMATH_H
extern “C”
{
int Summary(int n);
int Factorial(int n);
}
#endif
将该头文件包含在使用动态连接库的源文件中,连接应用程序时会连接上该引入库。这样,应用程序就可以象使用静态连接库一样自由的使用动态连接库中的函数了。注意要把动态连接库拷贝到应用程序可执行文件所在的目录(/TEST/DEBUG)下。
这是一种常用的方法。实际上,应用程序就是通过这种方式访问Windows的API函数的。Windows为其内核动态连接库生成引入库并提供了头文件。应用程序在编译时将引入库的信息带入可执行文件中,在运行时通过引入库信息访问API函数。
2. 直接指定库和函数地址
这种方式适合于一些提供文件格式转换等服务的动态连接库。比如,一个程序带有多个动态连接库,分别用于访问JPG、BMP、GIF等多种图像文件格式,这些动态连接库提供了相同的库函数接口。此时,无法使用引入库方式指定库函数。可以采用下面的方法来解决这个问题。
HANDLE hLibrary;
FARPROC lpFunc;
int nFormat;
if(nFormat==JPEG)//如果是JPEG格式,装入JPEG动态连接库
{
hLibrary=LoadLibrary(“JPEG.DLL”);
}
else//是GIF格式
hLibrary= LoadLibrary(“GIF.DLL”);
if(hLibrary>=32)
{
lpFunc=GetProcAddress(hLibrary,”ReadImage”);
if(lpFunc!=(FARPROC)NULL)
(*lpFunc)((LPCTSTR)strFileName);
FreeLibrary(hLibrary);
}
LoadLibrary函数装入所需的动态连接库,并返回库的句柄。如果句柄小于32,则载入库失败,错误含义参见有关手册。GetProcAddress函数使用函数名字取得函数的地址。利用该函数地址,就可以访问动态连接库的函数了。
FreeLibrary通过检查动态连接库的引用计数器,判断是否还有别的程序在使用这个动态连接库。如果没有,就从内存中移去该动态连接库;如果有,将动态连接库的使用计数器减1。LoadLibrary则将引用计数加1。
在用户动态连接库中,也可以使用MFC类。这时,可以选择静态连接和动态连接两种方式使用MFC库。
9.3.2 MFC扩展类库(_AFXDLL)
除了创建具有C语言接口的用户动态连接库外,MFC还允许用户在动态连接库中创建MFC类的派生类,这些类作为MFC类的自然延伸出现,可以为其他MFC应用程序所使用,就象使用普通的MFC类一样。
创建扩展类库
要创建扩展类库,可以选择File->New菜单,在Projects类型中选择MFC AppWizard(dll)。弹出MFC AppWizard 1of 1对话框,从中选择MFC Extension DLL(using shared MFC DLL)。AppWizard就会生成Extension DLL所需的框架。
这里不再创建动态连接库,而是用Visual C++的例子DLLHUSK程序(在SAMPLES/MFC/ADVANCED /DLLHUSK目录下)说明扩展类库的创建和使用。
在DLLHUSK项目工作区中,包含三个工程:DLLHUSK,TESTDLL1,TESTDLL2。
TESTDLL1和TESTDLL2分别定义了几个扩展类:CTextDoc、CHelloView和CListOutputFrame,DLLHUSK是使用这些类的示例程序。
在CListOutputFrame声明中,要加入AFX_EXT_CLASS,表明它是一个MFC扩展类。
class AFX_EXT_CLASS CListOutputFrame:public CMDIChildWnd
{
...
}
在函数定义处,还要包含afxdllx.h头文件
// Initialization of MFC Extension DLL
#include "afxdllx.h" // standard MFC Extension DLL routines
类的成员函数使用与应用程序中类的使用大致相同。
在CListOutputFrame类定义文件中,还提供了一个C函数。它的函数声明在类头文件testdll2.h中:
// Initialize the DLL, register the classes etc
extern "C" AFX_EXT_API void WINAPI InitTestDLL2();
这个函数用于初始化动态连接库和注册类:
// Exported DLL initialization is run in context of running application
extern "C" void WINAPI InitTestDLL2()
{
// create a new CDynLinkLibrary for this app
new CDynLinkLibrary(extensionDLL);
// nothing more to do
}
另外,源文件中还需要提供一个DllMain函数:
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
这个函数作用与前面的DllEntryPoint类似。
扩展类库也需要一个DEF文件,这个文件包含了动态连接库中可用的函数信息。由于现在动态连接库包含的是类,因此在函数命名上与用户动态连接库有所不同。
EXPORTS
?AddString@CListOutputFrame@@QAAXPBD@Z
??_7CListOutputFrame@@6B@
??_GCListOutputFrame@@UAAPAXI@Z
?OnEditCut@CListOutputFrame@@IAAXXZ
?_messageEntries@CListOutputFrame@@0QBUAFX_MSGMAP_ENTRY@@B
??0CListOutputFrame@@QAA@XZ
??1CListOutputFrame@@UAA@XZ
?Clear@CListOutputFrame@@QAAXXZ
?OnEditClear@CListOutputFrame@@IAAXXZ
?OnEditCopy@CListOutputFrame@@IAAXXZ
InitTestDLL2
......
有关函数名扩展的技术参考Visual C++帮助文档。
使用扩展类动态连接库
要使用扩展类库,要将类库的头文件包含在工程中。然后在适当位置初始化类库,DLLHusk是在InitInstance中完成这一工作的。
BOOL CHuskApp::InitInstance()
{
//...
InitTestDLL1();
InitTestDLL2();
//...
}
然后就可以象使用普通MFC类一样使用扩展类库中定义的类了。
m_pListOut=new CListOutputFrame;
访问DLL中的资源
当应用程序使用资源时,它按以下顺序查找资源:首先查找应用程序本身,看有没有对应的资源;如果没有,查找MFC400.DLL(或MFC400D.DLL,它包含调试信息)。再查找应用程序所带的动态连接库中的资源。如果想在DLL中直接使用资源而不经过以上搜索顺序,可以使用AfxGetResouceHandle()和AfxSetResourceHandle()函数。
AfxGetResourceHandle()和AfxSetResouceHandle()函数分别用来保存旧的资源句柄和设置新的资源句柄。比如,要想直接从DLL中载入一个位图资源,可以这么调用:
CBitmap mybitmap;
HINSTANCE hInstOld=AfxGetResourceHandle()
AfxSetResouceHandler(extensionDLL.hModule);
if(!mybitmap.LoadBitmap(IDR_BITMAP));
{
//restore the old resouce chain and return error
AfxSetResouceHandle(hInstOld);
return FALSE;
}
AfxSetResouceHandle(hInstOld);
//use this bitmap...
return TRUE;
还可以使用FindResource()搜索资源表,寻找给定的资源。
HRSRC FindResource(
HMODULE hModule,
LPCTSTR lpName,
LPCTSTR lpType
);
FindResource带三个参数,第一个参数是模块句柄,第二个是要查找的资源名字,如“MYDIALOG”,第三个是资源类型,可参见Visual C++文档。如果查找成功,则返回该资源句柄。可以用LoadResouce以该句柄为参数装入资源.
小 结
本章介绍了用户模块的创建和使用。
用户模块是由用户自己开发的、可以加入到最终用户应用程序中提供某一特定功能的函数和类的集合。
用户模块包括静态连接库和动态连接库两大类:静态连接库将函数的目标代码直接连入到应用程序中;动态连接库只是给出函数入口信息,在调用时访问DLL文件中函数的目标代码。
创建静态连接库:指定工程类型为Win32 Static Library,加入函数声明和定义,并编译和连接。提交函数库时只需要提供函数的lib文件和头文件。要使用静态库,可以将函数库和头文件包含在工程文件中。
创建动态连接库:提供函数定义、声明以及包含DLL文件函数信息的DEF文件。使用时需要将DLL文件拷贝至适当目录下。
两类动态连接库的创建:用户动态连接库和MFC扩展库。