9.1 动态链接库

动态链接库为模块化应用程序提供了一种方式,使得更新和重用程序更加方便。当几个应用程序在同一时间使用相同的函数时,它也帮助减少内存消耗,这是因为虽然每个应用程序有独立的数据拷贝,但是它们的代码是共享的。

9.1.1 动态链接库的概念

动态链接库是应用程序的一个模块,这个模块用于导出一些函数和数据供程序中的其他模块使用。应该从以下3个方面来理解这个概念:
(1)动态链接库是应用程序的一部分,它的任何操作都是代表应用程序进行的。所以动态链接库在本质上与可执行文件没有区别,都是作为模块被进程加载到自己的空间地址的。
(2)动态链接库在程序编译时并不会被插入到可执行文件中,在程序运行时整个库的代码才会调入内存,这就是所谓的"动态链接"。
(3)如果有多个程序用到同一个动态链接库,Windows在物理内存中只保留一份库的代码,仅通过分页机制将这份代码映射到不同的进程中。这样,不管有多少程序同时使用一个库,库代码实际占用的物理内存永远只有一份。

动态链接库(Dynamic Link Libraries)的缩写是DLL,大部分动态链接库镜像文件的扩展名为dll,但扩展名为其他的文件也有可能是动态链接库,如系统中的某些exe文件、各种控件(*.ocx)等都是动态链接库。

9.1.2 创建动态链接库工程

创建动态链接库工程的基本步骤同创建其他工程区别不大。
(1)新建工程09DllDemo,工程类型选择Win32 Dynamic-Link Library,如图。
在这里插入图片描述
(2)单击OK按钮,VC++弹出要求选择动态链接库类型的对话框。这些选择会影响VC++最终自动产生的框架代码,这里选中第三个选项A DLL that exports some symbols,即要求VC++自动生成一些导出符号代码(便于学习)。单击Finish按钮,完成工程创建。

现在打开09DllDemo.cpp文件,会看到DllMain函数,这个函数是动态链接库的入口点。库的入口函数仅供操作系统使用,Windows在库装载、卸载、进程中线程创建和结束时调用入口函数,以便动态链接库可以采取相应的动作。DllMain函数的框架结构为:

BOOL APIENTRY DllMain( HMODULE hModule,  //本模块句柄
                       DWORD  ul_reason_for_call,  //调用的原因
                       LPVOID lpReserved  //没有被使用
                     )
{
    switch (ul_reason_for_call)
    {
    //动态键接库刚被映射到某个进程的地址空间 
    case DLL_PROCESS_ATTACH: 
    //应用程序创建了一个新的线程
    case DLL_THREAD_ATTACH:
    //应用程序某个线程正常终止
    case DLL_THREAD_DETACH:
    //动态键接库将被卸载 
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

hModule参数是本DLL模块的句柄,即本动态链接库模块的实例句柄,数值上是系统将这个文件的映像加载到进程的地址空间时使用的基地址。需要注意的是,在动态链接库中,通过 "GetModuleHandle(NULL)"语句得到的是主模块(可执行文件映象)的基地址,而不是DLL文件映像的基地址。

ul_reason_for_call参数的值表示本次调用的原因,可能是下面4种情况中的一种:
(1)DLL_PROCESS_ATTACH:表示动态链接库刚被某个进程加载,程序可以在这里做一些初始化工作,并返回TRUE表示初始化成功,返回FALSE表示初始化出错,这样库的装载就会失败。这给了动态链接库一个机会来阻止自己被装入 。
(2)DLL_PROCESS_DETACH:此时则相反,表示动态链接库将被卸载,程序可以在这里进行一些资源的释放工作,如释放内存、关闭文件等 。
(3)DLL_THREAD_ATTACH:表示应用程序创建了一个新的线程 。
(4)DLL_ THREAD _DETACH:表示某个线程正常终止 。

DllMain函数以外的代码都是例子,可以删除。如果创建工程时第2步选择了第二个选项A simple DLL project,VC++将不会自动生成这些例子;如果选择了第一个选项An empty DLL project,VC++将创建空的工程,需用户自己来添加文件。

9.1.3 动态链接库中的函数

DLL能够定义两种函数,导出函数和内部函数。导出函数可以被其他模块调用,也可以被定义这个函数的模块调用,而内部函数只能被定义这个函数的模块调用。

动态链接库的主要功能是向外导出函数,供进程中其他模块使用。动态链接库中代码的编写没有特别之处,还要包含头文件还可以使用资源、使用C++类等。

作为示例,这里仅让09DllDemo导出一个简单的函数ExportFunc。将09DllDemo.cpp文件中的代码修改如下。

#include "stdafx.h"
#include "09DllDemo.h"
#include <stdio.h>

HMODULE g_hModule;

BOOL APIENTRY DllMain( HANDLE hModule, 
                       DWORD  ul_reason_for_call, 
                       LPVOID lpReserved
					 )
{
	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		g_hModule = (HMODULE)hModule; // 保存模块句柄
		break;
	}
	return TRUE;
}

// 自定义导出函数
void ExportFunc(LPCTSTR pszContent)
{
	char sz[MAX_PATH];
	::GetModuleFileName(g_hModule, sz, MAX_PATH);
	::MessageBox(NULL, pszContent, strrchr(sz, '\\') + 1, MB_OK);
}

在进程加载此DLL时,系统会调用DllMain函数,且ul_reason_for_call的值为DLL_PROCESS_ATTACH,这时上面的代码将模块句柄hModule保存到全局变量g_hModule中。GetModuleFileName函数用于取得指定模块的文件名,C运行库的字符串函数strrchr会找到文件名中最后的"\"字符串的位置,所以"strrchr(sz, '\\') + 1"的返回值是本DLL模块的名称(不带目录)。

函数定义完后只能在本工程中使用,要想将函数导出供其他模块调用,最简单的方法是在09DllDemo.h文件中进行声明。打开这个文件,将里面的代码修改如下。

#ifdef MY09DLLDEMO_EXPORTS
#define MY09DLLDEMO_API __declspec(dllexport)
#else
#define MY09DLLDEMO_API __declspec(dllimport)
#endif

// 声明要导出的函数
MY09DLLDEMO_API void ExportFunc(LPCTSTR pszContent);

MY09DLLDEMO_API宏是VC++自动生成的,在这里它被解释为__declspec(dllexport),说明此函数(ExportFunc)将从DLL模块中导出。编译连接程序,最后09DllDemo工程产生的文件中有3个可以被其他工程使用:09DllDemo文件夹下的09DllDemo.h头文件、debug文件夹(或者Release文件夹)下的09DllDemo.dll文件和09DllDemo.lib文件。

.dll文件就是动态链接库,.lib文件是供程序开发用的导入库,.h文件包含了导出函数的声明。

9.1.4 使用导出函数

调用DLL中的导出函数有两种方法:
(1)装载期间动态链接。模块可以像调用本地函数一样调用从其他模块导出的函数(API函数就是这样调用的)。装载期间链接必须使用DLL的导入库(.lib文件),它为系统提供了加载这个DLL和定位DLL中的导出函数所需的信息。
(2)运行期间动态链接。模块也可以使用LoadLibrary或者LoadLibraryEx函数在运行期间加载这个DLL。DLL被加载之后,加载模块调用GetProcAddress函数取得DLL导出函数的地址,然后通过函数地址调用DLL中的函数。

使用第1种方法时,9.1.3小节最后生成的3个文件都会被用到,使用第2种方法时,只有09DllDemo.dll文件会被用到。下面分别介绍它们。

1.装载期间动态链接
所谓装载期间动态链接,就是应用程序启动时由载入器(加载应用程序的组件)载入09DllDemo.dll文件。载入器如何知道要载入哪些DLL呢?这些信息记录在可执行文件(PE文件)的.idata节中。使用这种方法不用自己写代码显式地加载DLL。

新建一个名称为09ImportDemo的Win32控制台工程,然后将09DllDemo.h、09DllDemo.lib 和09DllDemo.dll 三个文件拷贝到09ImportDemo目录下。下面是调用导出函数ExportFunc的示例代码。

#include <windows.h>
#include "09DllDemo.h" 
//指明要链接到09DllDemo.lib库
#pragma comment(lib, "09DllDemo") 

void main()
{
        // 像调用本地函数一样调用09DllDemo.dll库的导出函数 
        ExportFunc("大家好!");
}

运行程序,弹出一个对话框.
在这里插入图片描述
#pragma命令指明要链接到09DllDemo.lib库。也可以不使用这条语句,直接将09DllDemo.lib文件添加到工程中,效果是一样的。

发步软件时必须将该软件使用的DLL与主程序一起发步。如果现在打开文件夹\09ImportDemo\Debug,试图运行09ImportDemo.exe程序,将会出现找不到09DllDemo.dll的错误。原因是没有把09DllDemo.dll文件拷贝到09ImportDemo.exe所在的目录下。把09DllDemo.dll和09ImportDemo.exe放在同一目录下再运行就一切正常了。

载入器加载DLL文件时,默认情况是在应用程序的当前目录下查找,如果找不到就会到系统盘"\windows\system32"文件夹下查找,如果还找不到就按错误处理。如果在VC++中启动09ImportDemo.exe程序,VC++会把程序的当前目录设为工程所在目录,而这个目录下有09DllDemo.dll文件,所以不会有错误发生。

这种方法加载DLL库的缺点很明显,如果用户丢失了DLL文件,那么程序是永远也不能启动了。所以很多时候要采取运行期间动态链接的方法,以便决定加载失败后如何处理。

2.运行期间动态链接
运行期间动态链接是在程序运行过程中显式地去加载DLL库,从中导出需要的函数。为了能够运行期间动态地导出函数,一般需要在09DllDemo工程中建立一个DEF文件来指定要导出的函数。可以这样向工程中添加DEF文件:

打开09DllDemo工程,选择菜单命令“File/New…”,在弹出的New对话框中选择Text File选项,输入文件名DllDemo.def,如果9.3所示,单击OK按钮即可。
在这里插入图片描述
在新添的DEF文件中写入如下的内容。

EXPORTS  
    ExportFunc 

现在回到09ImportDemo工程中来,把程序改为。

#include <windows.h>
// 声明函数原形
typedef void (*PFNEXPORTFUNC)(LPCTSTR);

int main(int argc, char* argv[])
{
        // 加载DLL库
        HMODULE hModule = ::LoadLibrary("C:\\Users\\sobey\\source\\repos\\09DllDemo\\Debug\\09DllDemo.dll");
        if(hModule != NULL)
	{ 
		// 取得ExportFunc函数的地址
		PFNEXPORTFUNC mExportFunc = (PFNEXPORTFUNC)::GetProcAddress(hModule, "ExportFunc");
		if(mExportFunc != NULL)
		{
			mExportFunc("大家好!");
		}

		// 卸载DLL库
		::FreeLibrary (hModule);	
	}
	
        return 0;
}

运行程序,效果和前面一样,也会弹出一个小对话框。

调用DLL导出函数应分两步进行。
(1)加载目标DLL,如下面代码所示。

HMODULE hModule = ::LoadLibrary("..\\09DllDemo\\Debug\\09DllDemo.dll"); 

LoadLibrary函数的作用是加载指定目录下的DLL库到进程的虚拟地址空间,函数执行成功返回此DLL模块的句柄,否则返回NULL。事实上,载入器也是调用这个函数加载DLL的。

在不使用DLL模块时,应该调用FreeLibrary函数释放它占用的资源。

(2)取得目标DLL中导出函数的地址,这项工作由GetProcAddress函数来完成。

FARPROC GetProcAddress(  
   HMODULE hModule, // 函数所在模块的模块句柄  
   LPCSTR lpProcName // 函数的名称 
); 

函数执行成功返回函数的地址,失败返回NULL。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值