动态链接库(DLL)相关概念与使用
动态链接库(DLL)相关概念与使用

动态链接库(DLL)相关概念与使用

静态链接库(lib)与动态链接库(dll)

我们经常把常用的代码制作成一个可执行模块供其他可执行文件调用,这样的模块称为链接库,分为动态链接库和静态链接库。

对于静态链接库LIB文件,LIB包含具体实现代码且会被包含进EXE中,导致文件过大,浪费磁盘和内存;

对于动态链接库DLL文件,DLL不必被包含在最终的EXE中,EXE执行时可以动态地装载和卸载DLL文件。

你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。

DLL的介绍与其优势

DLL的全称是Dynamic Link Library,中文叫做“动态链接文件”,在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。一个应用程序可使用多个DLL文件,一个DLL文件也可能被不同的应用程序使用,这样的DLL文件被称为共享DLL文件。

DLL文件中存放的是各类程序的函数(子过程)实现过程,当程序需要调用函数时需要先载入DLL,然后取得函数的地址,最后进行调用。使用DLL文件的好处是程序不需要在运行之初加载所有代码,只有在程序需要某个函数的时候才从DLL中取出。另外,使用DLL文件还可以减小程序的体积。

通过使用 DLL,程序可以实现模块化,由相对独立的组件组成。 例如,一个计帐程序可以按模块来销售。 可以在运行时将各个模块加载到主程序中(如果安装了相应模块)。 因为模块是彼此独立的,所以程序的加载速度更快,而且模块只在相应的功能被请求时才加载。

此外,可以更为容易地将更新应用于各个模块,而不会影响该程序的其他部分。 例如,您可能具有一个工资计算程序,而税率每年都会更改。 当这些更改被隔离到 DLL 中以后,您无需重新生成或安装整个程序就可以应用更新。

DLL的重要概念

1.DLL 的编制与具体的编程语言及编译器无关,只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是Visual Basic、Visual C++还是Delphi。

2.动态链接库随处可见,我们在Windows目录下的system32文件夹中会看到kernel32.dll、user32.dll和gdi32.dll,windows的大多数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面; gdi32.dll中的函数则负责图形方面的操作。一般的程序员都用过类似MessageBox的函数,其实它就包含在user32.dll这个动态链接库中。由此可见DLL对我们来说其实并不陌生。

3.Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL 包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。

4.库(Library)不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行。

5.库提供一些可以给别的程序调用的东西,别的程序要调用它必须以某种方式指明它要调用之。

DLL的使用(VS中创建DLL)

下面,我会通过介绍一个简单的实例操作,说明在Visual Studio中编写C++程序时使用dll的方法。
通过这个实例,我们将编写一个提供一个加法函数的dll文件,和调用该dll文件的exe可执行文件。
其中有一些操作,只是为了方便调试和演示,仅供参考。

创建项目

首先来到visual studio2022当中,创建新项目:

选择C++ for windows的空项目

我将工程命名为DLLTest,并将解决方案与项目文件放在同一文件夹

右击源文件文件夹,添加一个新建项

选择C++文件并添加

右击左边解决方案管理器中的解决方案,添加一个新建项目

找到动态链接库(DLL)

我将项目名称命名为AddFuncDLL

编写DLL

在弹出的dllmain.cpp文件中,添加以下代码:

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

extern "C" __declspec(dllexport)int add(int x, int y);//声明以C语言的处理方式,dll导出add函数

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;
}

//add函数声明
int add(int x, int y) {
    return x + y;
}

这里有几个要解释的点

extern "C" __declspec(dllexport)int add(int x, int y);这行代码当中extern "C"的作用是声明当前行以C语言的方式处理和编译,更多关于extern "C"的信息,请阅读参考文章7。在其后的int add(int x, int y)便是我们要导出的函数的标识,在下面则要有该函数对应的声明。

__declspec(dllexport)关键字的作用是声明从 DLL 中导出数据、函数、类或类成员函数,之所以要使用该关键字,是因为某些情况下,我们希望一些函数或数据只在dll内部使用,我们只希望其中一些函数提供给外部(即引用dll的程序)使用。关于__declspec(dllexport)关键字的更多信息,请阅读参考文章8、9、10

中间的:

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;
}

我们应当发现,这块代码在一开始就出现在我们的文件当中,这是一个DLLMain函数。

Windows在加载DLL的时候,需要一个入口函数,就如同控制台 或DOS程序需要main函数、WIN32程序需要WinMain函数一样。在前面的例子中,DLL并没有提供DllMain函数,应用工程也能成功引用 DLL,这是因为Windows在找不到DllMain的时候,系统会从其它运行库中引入一个不做任何操作的缺省DllMain函数版本,并不意味着 DLL可以放弃DllMain函数。

根据编写规范,Windows必须查找并执行DLL里的DllMain函数作为加载DLL的依据,它使得DLL得以保留在内存里。这个函数并不属于导出函数,而是DLL的内部函数。这意味着不能直接在应用工程中引用DllMain函数,DllMain是自动被调用的。

而最后一块的add函数则是我们手动编写的对外提供的函数。


编写主程序

准备好dll的cpp文件之后,我们就来编写我们DLLTest工程下的“源.cpp”文件,即要调用我们dll的程序:

在“源.cpp”文件中插入以下代码:

#include <Windows.h>
#include <iostream>
using namespace std;

int main() {
	HINSTANCE dllh = LoadLibraryA("AddFuncDLL.dll");//加载dll句柄
	int (*addFunc)(int, int);//声明对应的函数指针
	addFunc = (int (*)(int, int))GetProcAddress(dllh, "add");//从dll句柄中引入add函数
	cout << "1+1=" << addFunc(1, 1) << endl;
	return 0;
}

这段程序看起来相当简单,再此做一下解析

引入windows.h文件是为了使用句柄来引入dll文件,即使用HINSTACNCE类型。引入iosteam标准库则是为了打印。
main函数当中的第一行是使用LoadLibrary函数引入DLL文件并创建句柄。

LoadLibrary问题

说到LoadLibrary函数,细心的读者应该发现代码中多了一个A,这是因为,直接使用LoadLibrary函数时,预编译器会根据当前环境的编码,自动选择使用LoadLibraryW函数还是LoadLibraryA函数:

如果当前环境是UNICODE编码,则会自动使用LoadLibraryW函数,但是则会引发一个错误:

LoadLibraryW函数需要的是一个LPCWSTR类型的函数,而我们输入的字符串是char类型的指针(数组指针),不能自动转换为LPCWSTR(可以用一些其他的函数手动转化,具体百度),char数组只能自动转换为LPCSTR类型,使用LoadLibraryA函数的话就不需要考虑类型的问题。

关于这个问题,更多信息请参见参考文章11

这是VS版本留下的一些问题,具体还要看大家的环境。

函数指针声明

int (*addFunc)(int, int);声明了一个函数指针,关于函数指针的更多信息请阅读参考文章12。函数指针的声明方式比较特殊,不同于一般变量声明的类型+标识符,函数指针的声明方式变为了返回类型 (*变量名)(参数类型列表...)

从DLL句柄引入函数

addFunc = (int (*)(int, int))GetProcAddress(dllh, "add");这一行所作的,是调用GetProcAddress函数,返回一个函数指针,用其前的(int (*)(int, int))强制将返回值转换成addFunc对应的函数指针类型。GetProcAddress接受两个参数,第一个是DLL句柄实例变量,第二个则是要导入的函数名称。执行完该语句后,函数的地址便被存入了addFunc变量当中。

输出结果

最后用打印语句cout << "1+1=" << addFunc(1, 1) << endl;调用从dll获得的函数。


最终结果

首先在生成中生成解决方案,这一操作会将解决方案当中的所有的项目编译,并生成对应的目标文件。

然后点击本地windows调试器进行运行,可以看到结果:

注意事项

应用程序如何找到DLL文件?
使用LoadLibrary显式链接,那么在函数的参数中可以指定DLL文件的完整路径;如果不指定路径,或者进行隐式链接,Windows将遵循下面的搜索顺序来定位DLL:
(1)包含EXE文件的目录
(2)工程目录
(3)Windows系统目录
(4)Windows目录
(5)列在Path环境变量中的一系列目录

也就是说在某些情况下可以只指定要使用的dll文件名字,而不用给出完整的路径。

后记

DLL还有其他非常多的用途,如果有兴趣的话可以翻阅下面的参考文章

参考文章

1.什么是dll_创作都市-CSDN博客_dll文件是什么
2.VC++动态链接库(DLL)编程深入浅出(zz) – 中土 – 博客园 (cnblogs.com)
3.动态链接库(DLL)- xiaoluo91 – 博客园 (cnblogs.com)
4.LIB和DLL的区别与使用 – 远风工作室 – C++博客 (cppblog.com)
5.LPCWSTR_百度百科 (baidu.com)
6.什么是dll?dll是什么文件? – 知乎 (zhihu.com)
7.extern “C”:实现C++和C的混合编程 (biancheng.net)
8.__declspec(dllexport) – 简书 (jianshu.com)
9.dllexport、dllimport | Microsoft Docs
10.使用 __declspec(dllexport) 从 DLL 导出 | Microsoft Docs
11.LPCWSTR_百度百科 (baidu.com)
12.C++ 函数指针 & 类成员函数指针 | 菜鸟教程 (runoob.com)

0 0 投票数
打个分吧!
订阅评论
提醒
guest
2 评论
最多点赞
最新 最旧
内联反馈
查看所有评论
2
0
希望看到您的想法,请您发表评论x