windows API中的所有函数都包含在dll中。
其中三个最重要的dll:
1、Kernel32.dll:包含用于管理内存、进程和线程的各个函数。
2、User32.dll:包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数。
3、GDI32.dll:包含用于画图和显示文本的各个函数。
静态库:lib
函数和数据被编译进一个二进制文件中,在使用静态库的情况下,编译器在链接可执行程文件时,从静态库中复制这些函数和数据,并把他们与最终的应用程序的其他模块组合起来创建最终可执行的exe。
动态链接库的使用:
在使用动态链接库的时候,一般提供两个文件,一个dll,一个lib,该lib不是静态库,而是一个引入库。引入库中包含被dll导出的函数和变量的符号名,dll包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库即可,dll中的函数和数据并不复制到可执行文件中,在运行的时候,再去加载dll,访问dll中导出的函数。
使用动态链接库的好处:
1、使用多种编程语言来编写
2、增强产品的功能:开发规范。
3、提供二次开发的平台
4、简化项目的管理
5、可以节省磁盘空间和内存
6、有助于资源的共享
7、有助于实现应用程序的本地化
动态链接库加载的两种方式:
1.隐式链接
2.显示加载
////////////mydll.dll///////////////////////
_declspec(dllexport) int add(int a ,int b)
{
return a+b;
}
_declspec(dllexport) int sub(int a ,int b)
{
return a-b;
}
//////////////////////////////////////////
//VS环境下,查看dll导出的函数
dumpbin -exports mydll.dll
ordinal hint RVA name
1 0 0001107D ?add@@YAHHH@Z = @ILT+120(?add@@YAHHH@Z)
2 1 00011127 ?sub@@YAHHH@Z = @ILT+290(?sub@@YAHHH@Z)
为了支持函数重载,C++编译器在编译函数的时候,将函数名字都改了。
不同的C++编译改函数名字的方法不同,怎么办?后面再讲。
测试dll:
新建MFC程序,添加两个按钮:
//告诉编译器,这两个函数在外部定义。
extern int add(int a ,int b);
extern int sub(int a ,int b);
void Clesson19_dll_testDlg::OnBnClickedBtnAdd()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
str.Format (_T("5+3=%d"),add(5,3));
MessageBox(str);
}
void Clesson19_dll_testDlg::OnBnClickedBtnSub()
{
// TODO: 在此添加控件通知处理程序代码
CString str;
str.Format(_T("5-3=%d"),sub(5,3));
MessageBox(str);
}
编译出错,提示找不到xxx符号。因为没有提供引入库。
将前面生成的lib(引入库)引入进来即可。最简单的方法就是将该lib文件拷贝到解决方案所在的目录下,再在[输入|附加依赖项]中填写该lib库的名字即可。这样编译与链接通过。
可执行程序的输入信息:
dumpbin -imports xx.exe
优化
将
extern int add(int a ,int b);
extern int sub(int a ,int b);
//替换为:
_declspec(dllimport) int add(int a ,int b);
_declspec(dllimport) int sub(int a ,int b);
明确告诉编译器,我们声明的函数是一个dll中的引入库lib中,这样编译器可以生成执行效率更高的代码。
使用dll的时候,一般提供头文件,以告诉使用者,该dll导出了哪些函数。
即将:
_declspec(dllimport) int add(int a ,int b);
_declspec(dllimport) int sub(int a ,int b);
写入到一个头文件中即可。
下面我们来改造头文件,使头文件既可以为dll的使用者使用,也可以为dll本身所使用。
在dll工程中,写头文件如下:
//dll1.h
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif
DLL1_API int add(int a ,int b);
DLL1_API int sub(int a ,int b);
改造dll源文件:
//dll1.cpp
#define DLL1_API _declspec(dllexport)
#include "dll1.h"
int add(int a ,int b)
{
return a+b;
}
int sub(int a ,int b)
{
return a-b;
}
分析:编译的时候,头文件不参与编译。当编译dll源文件时,定义了DLL1_API,然后再包含dll.h的时候,发现已经定义了DLL1_API,所以什么也不做,此时,dll1.h中的DLL1_API表示的是,_declspec(dllexport)表示要导出的函数。
当dll的使用者包含该头文件的时候,只要他的项目中没有定义DLL1_API,则#define DLL1_API _declspec(dllimport) ,所以DLL1_API代表的是_declspec(dllimport)
导出C++中的类:
//dll1.h:
class DLL1_API Point
{
public:
void output(int x,int y);
};
//dll1.cpp:
#include <Windows.h>
#include <stdio.h>
#include "dll1.h"
void Point::output(int x,int y)
{
//获取前景窗口的句柄,就是用户当前工作线程中的工作窗口句柄
HWND hwnd = GetForegroundWindow();
HDC hdc = GetDC(hwnd);
char buf[20];
memset(buf,0,20);
sprintf(buf,"x=%d,y=%d",x,y);
//VS2010将项目的编码由使用Unicode字符集改为多字节字符集,第三个参数才不会报错。
TextOut(hdc,0,0, buf,strlen(buf));
ReleaseDC(hwnd,hdc);
}
如果我们只想导出某个类中的某个成员函数:
只需要将类后面的DLL1_API加到要导出的函数之前即可。
class Point
{
public:
DLL1_API void output(int x,int y);
void test();
};
如果是导出类,则类中所有的公开的成员函数都会被导出,如果只是导出了公开的成员函数,则只有该被导出的公开的成员函数才能够被访问。
//在MFC中测试dll
void Clesson19_dll_testDlg::OnBnClickedBtnOut()
{
// TODO: 在此添加控件通知处理程序代码
Point pt;
pt.output(5,3);
}
不管是导出类还是导出成员函数,类成员函数的访问方式是一样的,都是通过类的实例.成员函数才访问。
C++编译器在导出函数的时候会对函数的名称进行改变,以支持重载,这样,一种C++编译器导出的dll如果在另一种C++ 编译器环境下可能就不能使用,因为编译规则不同,可能找不到对应的函数名。
此时,我们最好手动加上extern “C” 限制编译器在编译函数的时候不要修改函数的名字。
在dll1.h中
//dll1.h
#ifndef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif
这个头文件是在dll工程中的,同时也是引用dll使用的,所以也要加上extern “C”告诉客户端dll中的导出函数的名字没有发生变化,所以在客户端编译环境下(不管是C编译器还是C++编译器)就会按照函数本来的名字去dll中找相应的函数。
在dll1.cpp中
//dll1.cpp
#define DLL1_API extern "C" _declspec(dllexport)
extern “C”的缺点:
1.不能导出类的一个成员函数,只能导出全局函数。
2.如果函数的调用约定发生改变的话,即使你使用extern “C”,函数的名字也会发生改变。
//dll1.h
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif
DLL1_API int _stdcall add(int a ,int b);
DLL1_API int _stdcall sub(int a ,int b);
//dll1.cpp
#define DLL1_API extern "C" _declspec(dllexport)
#include "dll1.h"
#include <Windows.h>
#include <stdio.h>
int _stdcall add(int a ,int b)
{
return a+b;
}
int _stdcall sub(int a ,int b)
{
return a-b;
}
_stdcall,即所谓的标准调用约定,也就是WINAPI,是Delphi调用约定(Pascal调用约定),即使定义了extern “C”,C++ 编译器在编译函数的时候也会改变函数的名字。
如果没有加_stdcall,则默认使用的是C语言的函数调用约定。
所以,如果我们写的dll要给Delphi客户端使用的时候,需要指定函数的调用约定为_stdcall。但是函数的名字终究是改变了。
解决方案:
模块定义文件,解决函数名字改变的问题:
//dll2.cpp
int add(int a,int b)
{
return a+b;
}
int sub(int a,int b)
{
return a-b;
}
//dll2.def
LIBRARY Dll2
EXPORTS //导出的函数的名字
add
sub
//如果还有muil() 可以这么写 multiply = muil 表示muil函数将会以multiply函数名导出。
这样的话,导出的函数的名字是不会发生改变的。
我们现在使用动态加载链接库的方式测试一下:
LoadLibrary();加载一个可执行的模块或者加载一个可执行程序。返回一个模块句柄。
得到导出的dll中的函数的地址
GetProcAddress();
动态加载dll,不需要头文件,也不需要lib文件。
优点:
在需要的时候加载dll,提高性能。
//lesson19_dll_testDlg.cpp
void Clesson19_dll_testDlg::OnBnClickedBtnAdd()
{
HINSTANCE hInst;
hInst = LoadLibrary(_T("G:\\Study\\Code\\孙鑫CPP\\Debug\\lesson19_dll2.dll"));
typedef int (*ADDPROC)(int a,int b);
ADDPROC ADD = (ADDPROC)GetProcAddress(hInst,"add");
if(!ADD)
{
MessageBox(_T("获取函数地址失败"));
}
CString str;
str.Format (_T("5+3=%d"),ADD(5,3));
MessageBox(str);
FreeLibrary(hInst);
}
此时,即使在dll中函数的调用约定改了,也不会影响函数名。
//dll2.cpp
int _stdcall add(int a,int b)
{
return a+b;
}
int _stdcall sub(int a,int b)
{
return a-b;
}
现在,我们的dll2.cpp中函数的调用约定改为_stdcall,此时生成的dll中的函数名仍为在def中指定的函数名。
但是在客户端调用的时候,一定要指定使用什么调用约定调用函数。
HINSTANCE hInst;
hInst = LoadLibrary(_T("G:\\Study\\Code\\孙鑫CPP\\Debug\\lesson19_dll2.dll"));
typedef int (_stdcall *ADDPROC)(int a,int b);
ADDPROC ADD = (ADDPROC)GetProcAddress(hInst,"add");
if(!ADD)
{
MessageBox(_T("获取函数地址失败"));
}
CString str;
str.Format (_T("5+3=%d"),ADD(5,3));
MessageBox(str);
FreeLibrary(hInst);
即在定义函数指针前面加上_stdcall。
使用动态加载dll的方式,使用dumpbin -imports xx.exe是看不出dll中的信息的。
我们还可以使用序号加载dll中的导出函数。
ADDPROC ADD = (ADDPROC)GetProcAddress(hInst,MAKEINTRESOURCEA(1));
一般情况下,我们还是使用函数名访问dll中的导出函数。虽然,使用序号可以在函数名即使发生改变的情况下也能够使用函数,但是写出的代码不易于阅读。
DllMain()函数,可选的入口点。
BOOL WINAPI DllMain(HANDLE hinstDLL, DWORD dwReason, LPVOID lpvReserved);
只要拷贝到cpp程序中即可,程序自动调用。
动态加载dll时,不使用时记得释放。
BOOL FreeLibrary( HMODULE hLibModule);