孙鑫VC学习(第19课--动态链接库)

来源:互联网 发布:java打包成jar供调用 编辑:程序博客网 时间:2024/06/05 14:38

动态链接库

n      自从微软推出第一个版本的Windows操作系统以来,动态链接库(DLL)一直是Windows操作系统的基础。

n      动态链接库通常都不能直接运行,也不能接收消息。它们是一些独立的文件,其中包含能被可执行程序或其它DLL调用来完成某项工作的函数。只有在其它模块调用动态链接库中的函数时,它才发挥作用。

n      Windows API中的所有函数都包含在DLL中。其中有3个最重要的DLLKernel32.dll,它包含用于管理内存、进程和线程的各个函数;User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;GDI32.dll,它包含用于画图和显示文本的各个函数。

静态库和动态库

n      静态库:函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)

n      在使用动态库的时候,往往提供两个文件:一个引入库和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。

使用动态链接库的好处

n      可以采用多种编程语言来编写。

n      增强产品的功能。

n      提供二次开发的平台。

n      简化项目管理。

n      可以节省磁盘空间和内存。

n      有助于资源的共享。

n      有助于实现应用程序的本地化。

动态链接库加载的两种方式

n      隐式链接

n      显示加载

下面是DLL的隐式连接。

选择WIN32动态链接库(工程名字:DLL1,空的工程,新建一个C++源文件,名字DLL1

查看导出的函数。(只有导出的函数,才可以被其他程序调用)

// #include <iostream>

// using namespace std; 注意:如果此时iostream为iostream.h会报错,虽然此段代码是注释起来的,但是应该这个知识点应该注意到,报错信息:(error C2871: 'std' : does not exist or is not a namespace) 其实这2行代码可要可不要

int addint(int a,int b)
{
return a+b;
}
int minint(int a,int b)
{
return a-b;
}

此时编译后,就可以看到在这个工程文件目录下面的DEBUG下面有一个后缀为DLL的文件,但是此时的DLL文件能否为外部调用,可以在下面查看一下:

第一种方法:在电脑 开始 的附件 的命令提示符下面的DOS窗口里面,进入你的DLL文件所在的目录,输入 dumpbin 指令,

第二种方法:把 C:\Program Files\Microsoft Visual Studio\VC98\Bin\VCVARS32.BAT  拖到命令提示符里面,就可以 输入 dumpbin 指令

如果第一种行不通,可以用第二种方法,第二种方法要按照你的电脑上面的具体路径来。,可以在DOS下面直接C:(或者c:大小写不分,直接进入到哪个盘下)

然后进入在DEBUG所在的目录下面,输入 dumpbin -EXPORTS SXLSN19SDLL.dll就可以看到这个DLL导出的函数名了。但是此时的结果表示没有任何的函数可以供导出使用,说明这个DLL没有导出任何函数。

此时改写函数为:

_declspec(dllexport) int addint(int a,int b)
{
return a+b;
}
_declspec(dllexport) int minint(int a,int b)
{
return a-b;
}

再查看,可以看到这个DLL导出这2个函数了。

Creating library Debug/SXLSN19SDLL.lib and object Debug/SXLSN19SDLL.exp

点.LIB 是引入库文件,里面有输出的函数的名字或者变量的符号名,.EXP是输出库文件,在这里并不重要

在DOS下看的结果如下解释:

ordinal是导出的函数的序号   hint是提示码,不重要      RVA列出的地址值,表示这2个函数在DLL中的什么位置可以找到   name表示导出的DLL的函数的名字(在C++中为了支持函数的重载,所以在编译链接的时候会更改函数的名字,进行名字改编,不同的C++编译器会采用不同的名字改编的规则去进行改编,这样函数就不具有通用性,如何解决?下面会说明)

下面进行调用测试,新建一个MFC的DIALOG,放2个BUTTON,一个ADD 一个SUB来调用刚刚写好的函数。

extern int addint(int a,int b);//说明从LIB中引入过来的
extern int minint(int a,int b);

或者下面2句也可以

_declspec(dllimport) int addint(int a,int b);//告诉编译器,是从DLL的LIB文件中引入的,编译器可以生成运行效率更高的代码
_declspec(dllimport) int minint(int a,int b);
void CDLLtestDlg::OnAdd() 
{
// TODO: Add your control notification handler code here
CString str;
str.Format("add result = %d",addint(1,2));
MessageBox(str);
}

void CDLLtestDlg::OnSub() 
{
// TODO: Add your control notification handler code here
CString str;
str.Format("sub result = %d",minint(1,2));
MessageBox(str); 
}

编译如下错误:

DLLtestDlg.obj : error LNK2001: unresolved external symbol "int __cdecl addint(int,int)" (?addint@@YAHHH@Z) //注意此时的错误是链接的时候出现,
DLLtestDlg.obj : error LNK2001: unresolved external symbol "int __cdecl minint(int,int)" (?minint@@YAHHH@Z)

//注意此时的错误是链接的时候出现,编译的时候因为有声明,所以不会报错,此时的函数名字就是在DOS下面的编译器给改编了名字的名字。

这是把F:\工作\code\myselfcode\SXLSN19SDLL\Debug\SXLSN19SDLL.lib  (此文件叫输入库文件,也叫引入库文件),把这个.LIB文件(里面有导出的函数的名字)复制到F:\工作\code\myselfcode\DLLtest目录下,然后再在DLLtest的工程下面的SETTING下面的LINK的Object/library modules:下面加上SXLSN19SDLL.lib,此时就可以链接过了。(注意此时仍需要有extern来把外部的函数声明一下).LIB中并不包含实际的代码,只是为链接程序提供信息,使在EXE文件中建立要用的重定位表。

在DEBUG的DOS的目录下,可以 dumpbin -imports DLLtest.exe 看到这个EXE所用的输入信息,此时运行的时候会说找不到这个SXLSN19SDLL.dll的路径,这是因为对一个动态链接库来说,当可执行模块执行时,系统为它分配了4GB的空间(因为启动后,会是一个进程)之后,加载程序会去分析可执行模块的输入信息,从输入模块会分析它的输入信息,从它的输入信息中找到它需要访问哪些DLL,然后在用户机器上面寻找DLL,进而加载DLL,搜索顺序:在可执行模块的DEBUG目录下,再是程序的当前目录下,再是SYSTEM32,再是SYSTEM,再是系统目录,再是PATH环境变量所列出的路径,所以为了让EXE能够运行,要让EXE能找到这个DLL,可以放到搜索目录下面的任何一个目录下,再运行,就可以了运行EXE了。如果在EXE中用到很多DLL,只要有一个DLL加载失败,EXE就会执行失败。也可以通过VISUAL 提供的工具看一个EXE要运行依靠哪些DLL,就是打开VC06的Depends工具,直接把DLL拉过去,(此工具也可以看EXE的所依赖的库),如果看到SXLSN19SDLL.dll是?的号,是因为EXE和DLL没有在同一个目录下。

一般给外部提供一个头文件,不然调用者不知道你的DLL中有哪些函数,在DLL的文件中新建立一个头文件,写下如下代码:

_declspec(dllimport) int addint(int a,int b);
_declspec(dllimport) int minint(int a,int b);

dllimport表明是从DLL中动态引入的

然后在调用的地方引入头文件

#include "..\SXLSN19SDLL\SXLSN19DLL.H"

可以看到,也可以出结果的。

下面改造一下,使得这2个DLL中函数既可以为DLL使用,又可以为调用这个DLL的程序使用:

在DLL的工程中的头文件中:

#ifdef DLL_H
#else
#define DLL_H _declspec(dllimport)
#endif

DLL_H int addint(int a,int b);
DLL_H int minint(int a,int b);

在DLL的实现文件中:

#define  DLL_H _declspec(dllexport)
int addint(int a,int b)
{
return a+b;
}
int minint(int a,int b)
{
return a-b;
}

分析:因为在编译的时候,头文件不参与编译,所以将CPP中的展开,DLL_H为_declspec(dllexport),在头文件中,因为此宏被定义过,所以此宏会为DLL_H为_declspec(dllexport)。这样在外面和CPP文件中就可以引入了。

在DLL中,只有导出的函数可以使用,如果没有导出,是不能使用的。下面看如何导出一个C++的类:

头文件中:

#ifdef DLL_H
#else
#define DLL_H _declspec(dllimport)
#endif

DLL_H int addint(int a,int b);
DLL_H int minint(int a,int b);

class DLL_H CPt//加了这个DLL_H宏以后,就可以导出这个整个类了,但是类里面的成员变量和成员函数仍然受限于它的访问权限
{
public:
void outpt();
CPt(int x=1,int y=2):x(x),y(y) {}
protected:
private:
int x;
int y;
};

.CPP文件中:

#define  DLL_H _declspec(dllexport)
//#include <Windows.h>//注意不能包含这一句,MFC的框架中已经包含了这句,如果再包含,重复包含,会报错
#include <afx.h>
#include "SXLSN19DLL.H"
int addint(int a,int b)
{
return a+b;
}
int minint(int a,int b)
{
return a-b;
}

void CPt::outpt()//将X 和Y输出在调用者的窗口上
{
HWND hNow = GetForegroundWindow();
HDC hDc = GetDC(hNow);
CString str;
str.Format("x=%d,y=%d",x,y);
TextOut(hDc,0,0,str,strlen(str));
}

此时在调用程序中编译链接运行,会报在DLL中找不到outpt的函数,这是因为在前面的操作中,我们在DEBUG下面放了先前的SXLSN19SDLL.dll(因为当时用depends看EXE文件的时候放的),所以在动态链接的时候,会优先在DEBUG下面找这个DLL,我们把DEBUG下面原先的DLL删除,此时再运行就可以出来结果了。此时用depends工具可以看到类中的函数被导出了。如果只想导出类中的某些函数,可以如下所示:

在DLL的头文件中:

#ifdef DLL_H
#else
#define DLL_H _declspec(dllimport)
#endif

DLL_H int addint(int a,int b);
DLL_H int minint(int a,int b);

class CPt//加了这个DLL_H宏以后,就可以导出这个整个类了,但是类里面的成员变量和成员函数仍然受限于它的访问权限
{
public:
//void DLL_H outpt();//这种和下面是同一个效果,都是一样的导出
DLL_H void outpt();
void outpt1();
CPt(int x=1,int y=2):x(x),y(y) {}
protected:
private:
int x;
int y;
};

因为C++编译器会对函数名字进行改编,使得函数不具有通用性,比如就不可以给C语言的使用了,为了不上这个名字改变,做如下更改:

_declspec(dllimport)
_declspec(dllexport)
前面都加上extern "C"  
但是extern "C"有一个缺陷:只能导出全局函数,不能导出一个类的成员函数,另外,如果一个函数的调用约定改变,改变成__stdcall后,函数的名字就算是用extern "C" 做了声明,函数的导出名字也会有所改变,在这种情况下,函数的声明和实现都要加上__stdcall后,不然会链接会报错,默认情况下,我们是__cdecl。__stdcall后的名字改编后的数字表明函数的参数占用了几个字节。

为了彻底解决名字改编的问题,用模块导出的方式,不需要头文件(事实上,加了头文件和不加头文件的效果一样,就算你加了头文件,系统也不会理会头文件),
LIBRARY SXLSN19SDLL
//SXLSN19SDLL是模块的内部名称,此名称要和DLL的名称匹配,另外,这句话也不是必须的
//用法详见MSDN
EXPORTS  
addint
minint 
//系统会用这里写的函数的名字导出这2个函数
注意:在def中不能加注释,在此处只是为了说明DEF的使用而加的注释

下面进行动态加载的测试:在DLL生成的工程中将原来的头文件删除,在测试程序中把#include头文件的那句删除,只留CPP和DEF,将使用DLL工程中的SETTING中的链接LIB的那个删除,然后写如下的语句:

void CDLLtestDlg::OnAdd() 
{
// TODO: Add your control notification handler code here
HINSTANCE hinstLib;
typedef int (*MYPROC)(int,int); 
MYPROC ProcAdd;
hinstLib = LoadLibrary("SXLSN19SDLL"); 
if (hinstLib != NULL)
{
ProcAdd = (MYPROC) GetProcAddress(hinstLib, "addint"); 
}
CString str;
str.Format("sub result = %d",ProcAdd (1,2));
MessageBox(str);
}

void CDLLtestDlg::OnSub() 
{
// TODO: Add your control notification handler code here
HINSTANCE hinstLib;
typedef int (*MYPROC)(int ,int ); 
MYPROC ProcAdd;
hinstLib = LoadLibrary("SXLSN19SDLL"); 
if (hinstLib != NULL)
{
ProcAdd = (MYPROC) GetProcAddress(hinstLib, "minint"); 
}
CString str;
str.Format("sub result = %d",ProcAdd (1,2));
MessageBox(str);
}

这种只需要在需要函数的时候动态加载,然后把函数映射到进程的内存空间,此时就算是_stdcall调用,也没有发生名字改编,这时,因为DLL的CPP文件中有 _stdcall ,所以在DLLTEST中,typedef int ( _stdcall *MYPROC)(int,int); 取函数时也要有_stdcall,不然调用不统一,会报错。

注:在DOS系统下,要想复制一部分内容,先标记,再点击右键,这时内容就已经到粘贴板上面了,然后就可以粘贴了。

或者在测试程序中,ProcAdd = (MYPROC) GetProcAddress(hinstLib, MAKEINTRESOURCE(1)); 1是DLL中的ordinal的序号。但是一般都用函数名这样更直观,不容易出错。

DllMain是DLL的入口函数,是可选的,可提供中不提供。如果要提供,可放在DLL的CPP中,如果有——当加载DLL的时候,就会调用dllmain函数,动态链接库的入口函数。可选,在写DLL时写与不写都可以。不要做太复杂调用。因为此时有些DLL还没有加载进来。下面自己写一个DLLMAIN的应用程序:
即拥有自己的DllMain()入口函数,则在build时会遇到类似如下的link错误:error LNK2005: _DllMain@12 already defined in xxx.OBJ。你只需要在工程设置里面把_USRDLL,删除,就可以正确编译了

#define  DLL_H  extern "C"  _declspec(dllexport)
//#include <Windows.h>//注意不能包含这一句,MFC的框架中已经包含了这句,如果再包含,重复包含,会报错
#include <afx.h>
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,  // handle to DLL module
DWORD fdwReason,     // reason for calling function
LPVOID lpReserved )  // reserved
{
    // Perform actions based on the reason for calling.
    switch( fdwReason ) 
    { 
case DLL_PROCESS_ATTACH:
MessageBox(GetForegroundWindow(),"DLL_PROCESS_ATTACH",
"DLL_PROCESS_ATTACH",MB_OK);//在本例中,在第一次加载DLL库的时候,会调用此
break;

case DLL_THREAD_ATTACH:
MessageBox(GetForegroundWindow(),"DLL_THREAD_ATTACH",
"DLL_THREAD_ATTACH",MB_OK);
break;

case DLL_THREAD_DETACH:
MessageBox(GetForegroundWindow(),"DLL_THREAD_DETACH",
"DLL_THREAD_DETACH",MB_OK);
break;

case DLL_PROCESS_DETACH:
MessageBox(GetForegroundWindow(),"DLL_PROCESS_DETACH",
"DLL_PROCESS_DETACH",MB_OK);
break;
    }
    return TRUE;  // Successful DLL_PROCESS_ATTACH.
}

int  _stdcall addint(int a,int b)
{
return a+b;
}
int  _stdcall minint(int a,int b)
{
return a-b;
}

在创建MFC的DLL时,第一个选项常规的DLL使用MFC的静态链接,在发布时,只提供DLL。第二个选项常规的DLL使用MFC的动态链接,发布DLL时,要确保用户机器上也有MFC的动态链接库,否则不能使用。第三个创建MFC的扩展DLL,这个可以导出MFC的类,在常规MFC的DLL中,不可以导出MFC的类,可以导出用户自己的类。


使用MFC编写的DLL,可以分成两大类:
规则DLL——规则(regular)DLL中所包含的函数,可以被所有Windows应用程序使用;
共享MFC——DLL中不包含MFC库函数,需要另外安装MFC动态链接库后才能使用;
静态MFC——DLL中包含MFC库函数,可以脱离MFC动态链接库独立使用。
扩展DLL——扩展(extension)DLL中所定义的类和函数,只能被所MFC应用程序使用。而且扩展DLL中不能包含MFC库函数,也需要另外安装MFC动态链接库后才能使用。(要在使用DLL的工程中包含头文件,不能使用MFC库函数,就算用了也没用)

原创粉丝点击