1、概念
(1)动态链接库广泛用于Windows系统及应用程序,不能单独被执行,在应用程序运行期间被动态调用的模块文件。区别于静态链接库,均属于独立的代码编译模块,但静态链接库在调用方编译期间,被编译到程序里,与程序形成完成的运行(exe)文件。动态链接库只是将对外导出信息(声明)暴露给调用方,调用方在程序中仅依据暴露的信息进行使用,在编译期间,不会与动态链接库发生任何关系,只有在应用程序运行动态链接库所包含的功能时,动态库才被加载到进程空间,实现调用过程。
(2)静态链接库只需要发布调用的主执行程序即可,动态链接库需要将相关的dll文件全部打包发布。windowsAPI就包含在3个主要的动态库中,分别是Kernel32.dll、User32.dll、GDI32.dll
2、优势
可以将完成的某种功能放到动态库中,供给任何需要调用的程序,实现模块化,便于修改维护和后期的功能完善,实现与主程序降低耦合。同时由于dll为编译的二进制文件,可以突破编写语言限制,管理简化。此外,dll文件只在内存加载一次,供不同程序共享,实现资源共享化,减少内存资源浪费,比如dll负责图标、对话框模板等资源加载,则多个应用程序可共享这些资源。
3、创建和使用
在VS下使用VC++创建动态库分为如下几种:
(1)Win32创建的DLL
生成文件及dllmain函数如下,windows下指定调用约定是__stdcall,意味着这个函数以标准Pascal的方式进行调用,其中传入dllmain的hMoudle为dll自身的模块句柄,ul_reason_for_call为调用原因说明,为分支下四种情况,一般不需要修改dllmain函数,在清除或者加载dll时可在此处做相关处理
在当前工程下添加导出信息的声明头文件,将来此文件在隐式链接dll时需要用到,需要在文件内添加导出声明_declspec(dllexport),由于dll工程会预定义宏XX_EXPORTS(可在属性配置中C++预定义下查看添加),所以为了调用方便,可根据宏判断分别定义导入导出声明的宏,这样在调用方就只需要包含此头文件就可使用导出数据,而不需要单独添加导入声明。接下来添加导出类、数据、函数等,需要注意导出声明的位置,类的导出声明放在class后。
此外,全局变量在cpp文件定义(只在所有调用的cpp中定义一次),如果是头文件声明需要在cpp处加external,如果在头文件添加external声明,这样不需要在任何其他文件调用时添加external,而且避免全局变量重复定义后编译错误
在不同语言C/C++编译条件下,导出函数的名称可能会出现变化,C++实现了函数重载,编译期间会对函数名进行修饰,所以在dll的显示加载(后文)时需要用到函数名的时候会出现问题,可以通过命令dumpbin查看dll文件的导出函数名,C函数编译后函数名不会出现变化,所以需要在导出声明时添加extern "C"告诉编译器以C的方式编译,下图是dumpbin运行情况:
同时,导出函数在调用时,函数参数的入栈顺序,堆栈清理以及函数名修饰等需要有统一的调用约定,参能实现函数正确调用,windows下一般是_stdcall调用约定,所以在导出函数时还需要添加此声明
实例代码如下,利用导出函数、全局数据、类实现数字相加功能
导出声明代码:
#pragma once
#ifdef TESTPRODLL_EXPORTS
#define TESTPRODLL2_API __declspec(dllexport)
#else
#define TESTPRODLL2_API __declspec(dllimport)
#endif
//extern "C" 指定以C语言方式调用函数,C语言没有函数重载,所以能保证函数名不变
//
//WINAPI约定调用函数的规则,指参数入栈顺序为_stdcall
extern TESTPRODLL2_API int a; //导出变量
extern "C" TESTPRODLL2_API int WINAPI Add(int b); //导出函数
class TESTPRODLL2_API ADD //导出类
{
public:
int WINAPI Do(int b);
};
extern "C" TESTPRODLL2_API int WINAPI GetA(); //获取全局变量
extern "C" TESTPRODLL2_API void WINAPI SetA(int b); //设置全局变量
extern "C" TESTPRODLL2_API int WINAPI RunADDDo(int a); //创建ADD对象,通过对外函数访问类
在cpp中实现如下:
#include "stdafx.h"
#include "TestAdd.h"
int a = 1; //全局变量定义需要加类型,而且必须定义
int Add(int b)
{
int c = a + b;
return c;
}
int ADD::Do(int b)
{
return a + b;
}
int GetA() //获取全局变量
{
return a;
}
void SetA(int b) //设置全局变量
{
a = b;
}
int RunADDDo(int a) //创建ADD对象,调用方通过对外函数访问dll的类
{
ADD add;
return add.Do(a);
}
编译工程后,win32的dll就创建完毕,目录如下:
接下来调用该dll,实现dll中的功能,分为如下两种方式:
(a)隐式加载dll
需要dll的导出头文件XX.h和引入库文件XX.lib,在调用cpp中包含XX.h头文件,同时添加lib链接,可以在属性配置中添加,也可用#progma coment命令引入。
隐式加载dll通过lib将导出声明信息编译到执行程序中,在程序启动后会加载lib指定的dll,dll需要放在执行程序目录下,不论dll功能是否正在使用,都会被加载
配置链接lib:
命令引入lib:
#pragma comment(lib,“XX.lib”)
以上两种链接lib方式均可,以下为第二种方式实现
#include "stdafx.h"
#include "windows.h"
#include "TestAdd.h"
#pragma comment(lib,".\\x64\\Debug\\TestPro.DLL.lib") //注意表示当前目录下需要加‘.\’
int main()
{
a = 2;
int c = Add(3); //5
ADD A;
int d = A.Do(4);//6
int m = RunADDDo(5); //7
return 0;
(b)显式链接dll
显式链接dll只需要dll文件本身即可,前提是调用者需要清楚dll内部导出细节,比如函数名等信息,需要将dll复制到调用执行文件的目录下
显式链接dll的好处是程序在执行dll相关功能时动态加载dll文件,启动后不会立马加载dll文件,所以不会影响启动效果,也能够节省内存空间
加载方式如下:
#include "stdafx.h"
#include "windows.h"
int main()
{
HINSTANCE hInstLibrary = ::LoadLibrary(L"TestPro.DLL.dll"); //加载文件
if (hInstLibrary == NULL)
{
::FreeLibrary(hInstLibrary);
return 0;
}
//
//*******dll中导出全局数据访问**********//
//
//通过函数指针进行访问
typedef int(__stdcall *FuncGetA)();
FuncGetA testGetA = (FuncGetA)::GetProcAddress(hInstLibrary, "GetA"); //根据函数名进行访问
int n = GetLastError();
if (nullptr == testGetA || 0 != n)
{
::FreeLibrary(hInstLibrary);
return n;
}
int a = testGetA(); //1
typedef void(__stdcall *FuncSetA)(int);
FuncSetA testSetA = (FuncSetA)::GetProcAddress(hInstLibrary, "SetA"); //根据函数名进行访问
n = GetLastError();
if (nullptr == testSetA || 0 != n)
{
::FreeLibrary(hInstLibrary);
return n;
}
testSetA(4);
a = testGetA(); //4
//
//*******dll中导出函数访问**********//
//
typedef int(__stdcall *FuncAdd)(int);
FuncAdd testAdd = (FuncAdd)::GetProcAddress(hInstLibrary, "Add"); //根据函数名进行访问
n = GetLastError();
if (nullptr == testAdd || 0 != n)
{
::FreeLibrary(hInstLibrary);
return n;
}
int sum = testAdd(5); //9
//
//*******dll中导出类访问**********// 备注:此功能在dll添加导出函数中实现dll中类访问
//
typedef int( *FuncCreateObj)(int);
FuncCreateObj testRun = (FuncCreateObj)::GetProcAddress(hInstLibrary, "RunADDDo"); //根据函数名进行访问
n = GetLastError();
if (nullptr == testRun || 0 != n)
{
::FreeLibrary(hInstLibrary);
return n;
}
sum = testRun(1); //5
::FreeLibrary(hInstLibrary); //卸载文件
return 0;
}
注意:显示加载要使用全局变量,需要在dll的头文件添加get/set函数进行操作,使用dll中的类需要添加对外导出函数进行间接调用
此外,对外声明可以通过另一种方式进行实现,需要使用def文件导出,def文件是模块定义文件,可以将需要导出的函数在此文件声明,编译后就将导出信息编译到lib文件,def文件使用如下
添加导出说明如下
;注释部分
LIBRARY TestPro.DLL.lib
EXPORTS
GetA @ 1
SetA @ 2
Add @ 3
RunADDDo @ 4
;导出函数名称,@序号为导出函数序号
同时修改导出头文件声明如下:
#pragma once
//extern "C" 指定以C语言方式调用函数,C语言没有函数重载,所以能保证函数名不变
//
//WINAPI约定调用函数的规则,指参数入栈顺序为_stdcall
extern int a; //导出变量
extern "C" int WINAPI Add(int b); //导出函数
class ADD //导出类
{
public:
int WINAPI Do(int b);
};
extern "C" int WINAPI GetA(); //获取全局变量
extern "C" void WINAPI SetA(int b); //设置全局变量
extern "C" int WINAPI RunADDDo(int a); //创建ADD对象,通过对外函数访问类
编译调用,可实现同样功能
以上代码均在VS2015下调试运行通过
(2)Win32创建支持MFC的DLL
创建如下:
如图,比win32下创建dll多了对外导出头文件和相关声明,同时主函数发生了变化,不是dllmian函数
而是main函数,增加了MFC支持,可以使用MFC相关类进行dll编程,dll实现包括调用等与win32基本一致,不多做描述
(3)MFC创建DLL
创建如下:
MFC使用了CWinApp类,添加了消息响应机制
以上是MFC创建dll时VS给出的所有内容,其他导出实现操作与win32基本一致,需要注意MFC下32位和64位的编译时不同,32位下会重定义错误,需要将WINAPI加到cpp中
以上是分享的dll相关的所有内容,欢迎转载,请注明出处!