第 10章 静态链接库和动态链接库在 Windows操作系统环境中,我们编写程序是离不开系统提供的库函数,
有些常规运算和系统调用等函数都是通过库函数方式提供的。 Windows库中函数都是可执行代码,其库类型主要有两种:
l 静态链接库 ( Static Link Library,即 Lib)
l 动态链接库 ( Dynamic-Link Library,即 DLL)
这两种库使用的主要区别是使用静态链接库的应用程序从函数库中得到所引用的函数的执行代码,然后把执行代码放进程序自身的执行文件中,
这样,应用程序在运行时就可以不再需要静态函数库的支持了;而动态链接库是一种用来为其它可执行文件(包括 EXE文件和其它 DLL)提供共享的函数库,通常我们编写的应用程序中需要使用 DLL的应用程序,我们可以调用 DLL中的导出函数( imported function),在我们应用程序本身的执行代码中并不包含这些函数的执行代码,它们经过编译和链接之后,独立的保存在 DLL中,使用 DLL的应用程序只包括了用于从 DLL中定位所引用的函数的信息,而没有函数具体实现,要等到程序运行时才从 DLL中获得函数的实现代码。显然,我们开发的使用了 DLL的应用程序在运行时必须要有相应的
DLL的支持。这是一种和过去常用的静态链接不同的方式。
静态链接库静态链接库的扩展名为,lib 。 如前所述,使用静态链接库的应用程序是从函数库中得到所引用的函数的执行代码,然后把执行代码放进程序自身的执行文件中,这样,应用程序在运行时就可以不再需要静态函数库的支持了,通常库中存放是常见的数学函数等 。
在许多高级编程语言里都有静态链接库,如 FORTRAN77有
Math.lib和 Fortran.lib两个静态链接库。目前,Windows编程主要使用动态链接库,但也有许多是使用静态链接库。
动态链接库目前,动态链接库 ( DLL) 在 Windows编程中得到了广泛的应用 。
Windows API函数中的相当一部分程序就是由一组 DLL所提供的,这些
DLL从安装 Windows之后起就存在于操作系统环境之中了 。 事实上,我们早就在使用 DLL进行编程了,只不过我们所使用的 DLL都是现成的,
并且所有调用 DLL的操作都由 Visual C++的编译和链接程序替我们完成的 。 使用 DLL与传统的静态链接库相比具有更多的优势 。
首先,动态链接库实现了多个应用程序共享数据和代码的方式 。 由于多个应用程序共享同一个 DLL中的函数,因而使用 DLL可以显著的节省磁盘空间 。 尤其对于 Windows应用程序,有很多的操作都是,标准化,了的,如果使用传统的静态链接,每一个需要完成这些操作的应用程序都必须在自己的执行文件中包括相同的执行代码,这不但使单个的应用程序变得更大,也浪费了磁盘空间 。
其次,由于上述原因,多个应用程序还可以同时共享动态链接库在内存中的同一份拷贝,这样就有效的节省了应用程序所占用的内存资源,减少了频繁的内存交换,从而提高了应用程序的执行效率和运行速度 。
再者,由于动态链接库是独立于可执行文件的,因此,如果需要向动态链接库中增加新的函数或是增强现有函数的功能,只要原有函数的参数和返回值等属性不变,那么,所有使用该 DLL的原有应用程序都可以在升级后的 DLL的支持下运行,而不需要重新编译 。 这就极大的方便了应用程序的升级和售后支持 。
另外,动态链接库除了包括函数的执行代码以外,还可以只包括如图标,位图,
字符串和对话框之类的资源,因此可以把应用程序所使用的资源独立出来做成
DLL。 对于一些常用的资源,把它们做到 DLL中后,就可为多个应用程序所共享 。
最后,动态链接库便于建立多语言的应用程序 。 我们可以把多语言应用程序中所使用的与语言相关的函数做到 DLL中,只要不同语言的应用程序所调用的函数都具有相同的接口,这样就可以通过简单地更换 DLL来实现多语言支持 。
然而,我们使用动态链接库也有其不足之处。最典型的就是应用程序在运行时必须要有相应的 DLL的支持。另外,使用 DLL也增大了程序运行的开销,但这种额外的开销对于大多数应用程序的影响并不是很明显,我们也只是在某些对运行速度要求苛刻的特殊场合,才不得不考虑这一点。
Visual C++ 6.0支持多种 DLL,包括:
l 非 MFCDLL
l 静态链接到 MFC的常规 DLL
l 动态链接到 MFC的常规 DLL
l MFC扩展 DLL
一般来说,非 MFC DLL( non-MFC DLL) 的内部不使用 MFC,非 MFC DLL的导出函数都使用标准的 C接口 ( standard C interface),调用非 MFC DLL提供的导出函数的可执行程序可以使用 MFC,也可以不使用 MFC。 其余三种 DLL的内部都使用了 MFC。
顾名思义,静态链接到 MFC的常规 DLL( regular DLL statically linking to MFC)
和 动态链接到 MFC的常规 DLL( regular DLL dynamically linking to MFC) 的区别在于一个使用的是 MFC的静态链接库,而另一个使用的是 MFC的 DLL。 这和一般的 MFC应用程序的情况是很类似的 。
MFC扩展 DLL一般用来提供派生于 MFC的可重用的类,以扩展已有的 MFC类库的功能。 MFC扩展 DLL使用 MFC的动态链接版本。只有使用 MFC动态链接的可执行程序(无论是 EXE还是 DLL)才能访问 MFC扩展 DLL。 MFC扩展 DLL的另一个有用的功能是它可以在应用程序和它所加载的 MFC扩展 DLL之间传递 MFC和 MFC派生对象的指针。在其它情况下,这样做是可能导致某些问题。
我们选择那一种 DLL的类型可以从以下几个方面来考虑:
l 相比之下,我们对使用 MFC的 DLL而言,非 MFC DLL显得更为短小精悍 。 因此,如果 DLL不需要使用 MFC,那么使用非 MFC
DLL是一个很好的选择,它将显著地节省磁盘和内存空间 。 同时,无论应用程序是否使用了 MFC,都可以调用非 MFC DLL中所导出的函数 。
l 如果需要创建使用了 MFC的 DLL,并希望 MFC和非 MFC应用程序都能使用所创建的 DLL,那么可以选择的范围包括静态链接到
MFC的常规 DLL和动态链接到 MFC的常规 DLL。 动态链接到 MFC的常规 DLL比较短小,因此可以节省磁盘和内存,但是,在分发动态链接到 MFC 的常规 DLL 时,必须同时分发 MFC的支持 DLL,如
MFCx0.DLL和 MSVCRT.DLL等 。 而使用静态链接到 MFC的常规 DLL
则不存在这种问题 。
如果希望在 DLL中实现从 MFC派生的可重用的类,或者是希望在应用程序和 DLL之间传递 MFC的派生对象时,必须选择 MFC扩展 DLL。
动态链接库 DLL中一般定义有两种类型的函数:导出函数
( export function) 和内部函数 ( internal function) 。 导出函数是可以被外部程序调用的函数,内部函数只能在 DLL内部使用 。 尽管 DLL类型各异,但每个 DLL都含有一个入口点函数 DllMain。
DllMain的作用是初始化 DLL,并在卸载时清理 DLL。
DLL文件与可执行文件非常相似,不同点在于 DLL包含有导出表
( Export Table) 。 导出表包含 DLL中每个导出函数的名字,这些函数是进入 DLL的入口点 。 只有导出表中的函数可以被外部程序调用 。 DLL中的导出表可以使用带 /EXPORTS开关的 DUMPBIN
工具来查看 。 从 DLL中导出函数有以下两种方法:
l 创建模块定义文件 (,DEF) 并在建立 DLL时使用,DEF文件 。
在导出函数的定义中使用关键字 __declspec(dllexport)。
模块定义文件 (,DEF) 是由一个或多个用于描述 DLL属性的模块语句组成的文本文件 。 如 果 不 使 用 关 键 字
__declspec(dllexport()导出 DLL函数,那么必须使用,DEF文件 。 每个,DEF文件至少必须包含以下模块定义语句:
l 第一个语句必须是 LIBRARY语句,这个语句指出
DLL的名字,链接器将这个名字放到 DLL导入库 ( import
library) 中,DLL导入库包含了指向外部 DLL的函数索引指针 。
l EXPORTS 语句列出被导出函数的名字,以及导出函数的数值 ( 由 @号与数字构成 ) 。 序数值可以省略,编译器 ( Compiler) 会为每个导出函数指定一个,但这样指定的值不如自己指定的明确 。
使用 DESCRIPTION语句描述 DLL的用途,这个语句可以省略。
静态链接库和动态链接库的使用如果是 Win32静态链接库,我们只要把静态链接库如 MyStaticLib.lib
和其头文件如 Square.h复制到客户的工程中,然后再选择菜单,工程,
( 即 Project) 下的,设置,(即 Setting)菜单项或按 Alt+F7弹出设置对话框,
选择,Link”选项卡,在,对象 /库模块,编辑框中指明静态链接库的路径如,D:\MyAppProject\MyStaticLib.lib”。
我们开发的应用程序与 DLL的链接有两种方法:一种是隐含链接,另一种为显式链接 。
隐含链接有时又称为静态加载 。 如果应用程序使用了隐含链接,操作系统在加载应用程序的同时加载应用程序所使用的 DLL。 显式链接有时又称为动态加载 。 使用动态加载的应用程序必须在代码中明确的加载所使用的 DLL,并使用指针来调用 DLL中的导出函数,在使用完毕之后,应用程序必须卸载所使用的 DLL。 同一个 DLL可以被应用程序隐含链接,也可以被显式链接,这取决于应用程序的目的和实现 。
隐含链接在使用隐含链接时除了需要相应的 DLL文件外,还必须具备如下的条件:
l 一个包括导出的函数或 C++类的头文件
l 一个输入库文件(,LIB文件)
通常情况下,我们需要从 DLL的提供者那里得到上述的文件。输入库文件是在
DLL文件被链接时由链接程序生成的。
如果我们应用上述建立的 MyDll.Dll库里的 MySin函数,我们需要在自己的应用程序模块,cpp文件中的开始处添加以下代码:
extern "C" __declspec(dllimport) double MySin(double x);
注意:这段代码我们可以添加在动态链接库的头文件中,因此这时的头文件和原来的头文件有所区别。创建 DLL时的 MyDll.h中使用的是 __declspec(dllexport)
关键字,而供应用程序所使用的 MyDll.h中使用的是 __declspec(dllimport)关键字。
无论创建 DLL时使用的是 DEF文件还是 __declspec(dllexport)关键字,均可使用
__declspec(dllimport)关键字从 DLL中导入函数。导入函数时也可以省略
__declspec(dllimport)关键字,但是使用它可以使编译器生成效率更高的代码。如果需要导入的是 DLL中的公用数据和对象,则必须使用 __declspec(dllimport)关键字。
建立一个应用时,我们需要先将动态库如 MyDll.Dll拷贝到 Windows的
System子目录下,然后将分发的的静态链接库如 MyDll.lib文件拷贝到应用工程所在的目录下 。 然后单击菜单项 Project|Settings...,然后将输入库文件如 MyDll.lib添加到 Link选项卡下的 Object/library modules文本框中 。 如果忽略这一步,链接时将会导致错误 。 完成之后即可创建该应用程序 。
一般情况下,程序在运行时,系统将按如下的顺序查找程序所使用的动态链接库:
1,系统预安装的 DLL,如 KERNEL32.DLL和 USER32.DLL等
2,当前目录
3,Windows的系统的目录,如 WINNT\system32
4,Windows所在的目录,如 WINNT
5,环境变量 PATH中所指定的目录显式链接如果没有与 DLL相关联的 LIB文件,则必须使用显式链接。使用显式链接同样必须知道函数返回值的类型和所传递的参数个数、类型和顺序。与使用隐含链接不同的是,使用显式链接的应用程序在调用 DLL
中的导出函数前,必须使用 LoadLibrary()函数加载
DLL并得到一个模块句柄。然后使用该句柄调用
GetProcAddress()函数获得所需调用的导出函数的指针,
并通过该指针调用 DLL中的导出函数,这种模式使用显式链接到 DLL的应用程序不再需要相应的 LIB文件。
在使用完毕之后,还需调用 FreeLibrary()函数释放加载的 DLL。
LoadLibrary()函数的参数是所调用的 DLL的名字,这个名字不是放入输入库文件中的名字,而是 DLL的文件名 。 如果文件的扩展名为,DLL,
则可以省略 。
注意:我们使用显式链接的应用程序刚运行时,所调用的 DLL并不运行,只有当应用程序调用 LoadLibray()时系统才加载相应的 DLL运行,并在应用程序调用 FreeLibrary()时卸载该 DLL。 我们在使用隐含链接的应用程序时调用 DLL中的导出函数,方法同调用一般的函数一样,而使用显式链接的应用程序必须使用指针来调用 。 由于使用了指针,因此在编译时不能验证参数的合法性,通过指针使用不合法的参数来调用 DLL中的导出函数将会导致不可预料的后果 。
很明显,使用隐含链接的方式调用 DLL中的导出函数要比使用显式链接方便得多。但在某些情况下我们必须使用显式链接。事实上,使用显式链接调用 DLL提供了更大的灵活性。尤其在没有与 DLL相对应的
LIB文件时,我们只能使用显式链接来调用 DLL中的导出函数,并且,
只要我们使用函数名作参数来调用 GetProcAddress(),在更新 DLL时,就没有必要重新链接应用程序。另外,使用隐含链接的方式的应用程序加载 DLL时如果发生错误(如 DLL文件未找到或是 DLL中的 DllMain()函数初始化失败)时,应用程序将被终止,而使用显式链接的应用程序则可以使用如上面的例子中所给出的方法来避免出现这种情况。
由于应用程序调用 LoadLibrary()函数时才加载
DLL,因此使用显式链接的应用程序的加载速度要比使用隐含链接的应用程序快。使用显式链接的另一个好处是,应用程序可以在运行时决定所加载的 DLL。但是我们要记住,由于使用了指针来传递应用程序的参数,因此编译器在编译时无法确认应用程序所传递的参数类型是否合法。传递不合法的参数给 DLL中的导出函数的一件危险的事。在程序调试的过程中我们一定需要注意这一点。