静态库
函数和数据被编译进一个二进制文件(.LIB)。在使用静态库下,在编译连接可执行文件时,链接器从库中复制这些函数和数据,并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.EXE)。当产品发布时,只需要发布可执行文件,不需要发布使用的静态库。
它的特点在于:
1.编译后的可执行文件包含了所需要的函数的代码,占用磁盘空间较大。(但是可以避免出现用户的电脑上没有你开发时所用的库的尴尬情形。)
2.如果多个调用相同库的进程在内存中同时运行,内存中会存放多份相同的代码。
动态库
在使用动态库的时候,往往提供两个文件:引入库(.lib)文件和DLL(.dll)文件。引入文件包含DLL导出的函数和变量的符号名,而.dll包含了该DLL的实际的函数和数据。再使用动态库的情况下,在编译连接可执行文件时,只需要连接该DLL的引入库文件,而该DLL的函数代码和数据并不复制到可执行文件中,知道可执行程序运行时,才加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。此时,在发布产品时,除了发布可执行文件外,还要发布程序将要调用的动态链接库。
使用动态库的好处在于能够节省磁盘空间和内存。如果多个应用程序需要访问同样的功能,那么可以将该功能以DLL的形式提供,这样一台机器上只需要存在一份该DLL就可以了,从而节省了磁盘空间。如果多个程序调用同一个DLL,该DLL的页面只需要存放在内存一次,所有的应用程序都可以共享它的页面了。
首先,我们新建一个空的Win32动态链接库,为其增加两个函数:
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
当build之后,在这个工程的Debug目录下,就会有一个对应于工程明的dll文件。这个dll目前是无法使用的,因为这两个函数都没有被“导出”。我们可以利用Visual Studio提供的命令行工具Dumpbin来查看:
D:\MyPrograms\mfc\CH_19_DLL1\Debug>dumpbin -exports CH_19_DLL1.dll
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.
Dump of file CH_19_DLL1.dll
File Type: DLL
Summary
7000 .data
1000 .idata
2000 .rdata
2000 .reloc
2A000 .text
其中并没有与函数有关的信息。
为了让DLL导出一些函数,需要在每个要被导出的函数前面加上标示符:_declspec(dllexport)。
此时在查看,就可以看到:
ordinal hint RVA name
1 0 0000100A ?add@@YAHHH@Z
2 1 00001005 ?subtract@@YAHHH@Z
其中?add@@YAHHH@Z和?subtract@@YAHHH@Z的意思在我的另一篇博客:《完全总结__cdecl __fastcall, __stdcall,__thiscall》中详细的讨论过,这里只说明如下:
首先,编译器会给函数名前加一个?;其次,因为是__cdecl调用,所以后面加上YA;再次,H代表的是int类型,3个H分别表示的是返回值,第一个参数,第二个参数,最后以@Z结尾。
有了动态链接库以后,我们就要在程序中加载它们。有两种可选的方式:隐式链接方式加载DLL,显示加载DLL。
我们先看隐式加载。我们新建一个MFC对话框应用程序,然后给上面放两个按钮,一个用来做加法,另一个用来做减法。消息响应函数如下:
extern int add(int a, int b);
extern int subtract(int a, int b);
void CCH_19_DllTestDlg::OnBtnAdd()
{
// TODO: Add your control notification handler code here
CString str;
str.Format("5 +3 = %d",add(5,3));
MessageBox(str);
}
void CCH_19_DllTestDlg::OnBtnSubtract()
{
// TODO: Add your control notification handler code here
CString str;
str.Format("5 - 3 = %d",subtract(5,3));
MessageBox(str);
}
在其中因为要调用动态链接库中的add和subtract函数,所以在这里声明为外部函数。
具体如何加载呢?
从我们的动态链接库的debug目录下,将*.lib文件复制到MFC程序所在的文件夹下,然后在工程->设置->链接中增加整个lib文件,程序就能编译链接通过了。我们可以使用Dumpbin来查看:
D:\MyPrograms\mfc\CH_19_DllTest\Debug>dumpbin -imports CH_19_D
Microsoft (R) COFF Binary File Dumper Version 6.00.8168
Copyright (C) Microsoft Corp 1992-1998. All rights reserved.
Dump of file CH_19_DllTest.exe
File Type: EXECUTABLE IMAGE
Section contains the following imports:
CH_19_Dll1.dll
4052C0 Import Address Table
40508C Import Name Table
0 time date stamp
0 Index of first forwarder reference
1 ?subtract@@YAHHH@Z
0 ?add@@YAHHH@Z
其他部分省略。
我们看到,这个可执行文件需要导入subtract和add两个函数。
此时,程序还是不能执行的,因为.exe文件并不知道从哪里获得.dll文件,此时,编译器会依次在当前可执行文件的目录(.exe所在的目录)、当前目录(.cpp所在的目录)、系统目录和环境变量目录中查找。我们可以将对应的.dll文件也拷过去。程序就能成功执行了。
除了使用Dumpbin之外,我们也可以使用vc++提供的一个可视化的工具:Depends来查看。这里就略去不提了。
除了使用extern之外,我们还可以使用__declspec(dllimport)来表明该函数是从动态链接库中加载的。即:
//extern int add(int a, int b);
//extern int subtract(int a, int b);
__declspec(dllimport) int add(int a, int b);
__declspec(dllimport) int subtract(int a, int b);
这只是一个说明性的例子,在实际中,我们通常不是这样编写程序的。因为当我们把dll交给用户以后,用户并不知道dll中拥有那些函数,只能通过前面介绍的Dumpbin和Depends等编译工具来猜测函数的原型,这是很不方便的。正确的做法是,为这个dll增加一个头文件,在头文件添加函数的声明及注释。在头文件添加:
__declspec(dllimport) int add(int a, int b);
__declspec(dllimport) int subtract(int a, int b);
注意,因为头文件是给用户使用的,所以是指明这些函数是从dll中导入的。而在我们的对话框程序中,添加:
#include "..\CH_19_Dll1\CH_19_Dll1.h"
有的时候,我们希望dll库中的函数不仅可以为客户使用,也能够让库自身调用,那么我们就要利用预编译指令来实现了,在头文件:
#ifdef DLL1_API
#else
#define DLL1_API __declspec(dllimport)
#endif
DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);
首先判断是否定义DLL1_API,如果定义,则什么也不做,否则就会将DLL1_API定义为__declspec(dllimport),然后用__declspec(dllimport)代替DLL1_API
而在源文件中:
#define DLL1_API _declspec(dllexport)
#include "CH_19_Dll1.h"
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
此时这些函数就跟我们之前写的函数一样了,可以互相调用
我们仔细分析一下。在编译该dll时,源文件会定义DLL1_API,然后将头文件展开。此时因为已经定义了DLL1_API,所以直接编译函数的声明,表明这个函数是从动态链接库中导出的。
而当用户的程序调用该dll时,只要用户程序中没有定义DLL1_API,那么就会定义DLL1_API为__declspec(dllimport)。
我们下面看看如何从动态链接库中导出C++类:
在头文件中声明:
class DLL1_API Point
{
public:
void output(int x, int y);
};
在源文件中定义:
void Point:: output(int x, int y)
{
//获得当前窗口的句柄
HWND hwnd = GetForegroundWindow();
//获取DC
HDC hdc = GetDC(hwnd);
char buf[20];
memset(buf,0,20);
sprintf(buf,"x = %d, y= %d", x, y);
TextOut(hdc,0,0,buf,strlen(buf));
ReleaseDC(hwnd,hdc);
}
注意到,这里使用了windows函数,和输出函数,所以得包含头文件Windows.h、stdio.h。
在应用程序中,新增一个按钮来调用这个类的成员函数:
void CCH_19_DllTestDlg::OnBtnOutput()
{
// TODO: Add your control notification handler code here
Point pt;
pt.output(5,3);
}
我们可以再利用Dumpbin看看这个dll:
1 0 00001014 ??4Point@@QAEAAV0@ABV0@@Z
2 1 0000100A ?add@@YAHHH@Z
3 2 0000100F ?output@Point@@QAEXHH@Z
4 3 00001005 ?subtract@@YAHHH@Z
其中的?4Point@@QAEAAV0@ABV0@@Z是构造函数,?output@Point@@QAEXHH@Z中,@Point是表明它是Point类的函数,QAE表示它是public函数,X表示返回值类型为void,HH表示有两个int类型的参数。
其实,我们完全可以不导出整个类,而是只导出其中的若干函数:
class Point
{
public:
void DLL1_API output(int x, int y);//导出该函数
void test();
};
下面的问题与前面的问题紧密相关,C++编译器生成dll时,会对导出的函数名进行改编,但是不同的编译器的改变规则不完全相同,特别的,如果是一个纯C的编译器序使用这个dll,则由于改编名字的不同,dll中的函数就不能被找到了。
因此,我们希望动态链接库文件在编译时,导出函数的名称不要发生改变。我们可以使用extern "C"来实现,指定的内容用C的方式编译:
#ifdef DLL1_API
#else
#define DLL1_API extern "C"__declspec(dllimport)
#endif
DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);
#define DLL1_API extern "C"_declspec(dllexport)
#include "CH_19_Dll1.h"
//#include
#include
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
注意,由于是使用的C语言,所以就得把类相关的代码注释掉了。我们再用dumpbin看看这个dll:
ordinal hint RVA name
1 0 0000100A add
2 1 00001005 subtract
此时,名字没有发生改编。
引起名字改编的还有调用约定,在默认情况下,使用的cdecl:参数由右向左压栈,由调用者清栈;假如我们改为_stdcall:参数由右向左,由被调用的函数自己清栈,那么函数的名字又会变为:
ordinal hint RVA name
1 0 00001005 _add@8
2 1 0000100A _subtract@8
我们可以通过模块定义文件来解决不同编译器、不同语言之间的编译器将函数明修饰的不一样的问题。我们重新建立一个动态链接库,为其增加一个.cpp文件:
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
然后,我们在它的目录下增加一个记事本文件,并把后缀名改为.def,使用vc++将其打开,添加如下代码:
LIBRARY CH_19_Dll2
EXPORTS
add
subtract
其中第一行是动态链接库的名称,EXPORTS语句表明的是将要导出的函数的名字,关于它的详细用法,可以参考MSDN。此时,导出函数的名称就统一了。
说了这么多,都是隐式加载的。我们现在看看如何显式加载,先看程序:
void CCH_19_DllTestDlg::OnBtnAdd()
{
// TODO: Add your control notification handler code here
HINSTANCE hInst;
//加载动态链接库
hInst = LoadLibrary("CH_19_Dll2.dll");
typedef int(*ADDPROC)(int a, int b);
//获取函数地址
ADDPROC Add = (ADDPROC)GetProcAddress(hInst,"add");
if(!Add)
{
MessageBox("获取函数地址失败");
return ;
}
CString str;
str.Format("5 +3 = %d",Add(5,3));
MessageBox(str);
}
注意,其中GetProcAddress的第二个参数,函数名通过.def指定。
我们看到,隐式加载实现起来更为简单,加载好以后就可以直接使用;而显示加载更为灵活,可以在需要时才加载dll。但是他的麻烦之处就在于一旦编译器生成的新的函数名发生变化,那么就得修改。比如,如果dll中的函数调用方式改为stdcall,那么这里的函数指针也得改为stdcall。而且如果dll并没有使用.def来指定导出的名字,那么这里就得使用编译器修改后的名字:?add@@YAHHH@Z,或者使用访问序号:
ADDPROC Add = (ADDPROC)GetProcAddress(hInst,MAKEINTRESOURCE(1));
这个访问序号也可以通过dumpbin获得
ordinal hint RVA name
1 0 00001005 ?add@@YAHHH@Z
当然,在实际应用中,建议还是使用名字比较好,毕竟有意义的名字好于序号。
其实,在windows加载dll时需要一个入口函数,就如同控制台或DOS程序需要main函数、WIN32程序需要WinMain函数一样。这个函数称为DllMain,声明如下:
BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved );
其中第二个参数是调用的原因,可以使用一个switch/case语句来对于每种情况分别处理。这个函数是一个可选的函数,由于我们的dll只完成一些简单的功能,所以没有使用。需要注意的是,在这个函数中不要进行太复杂的的调用。因为此时一些核心动态库,比如user32.dll或者GDI32.dll等还没有加载,如果我们的编写的DllMain函数需要调用这两个库的某些函数的话,则会出错。