概述
动态链接库(dll)是指在程序运行时动态加载的库文件。特点是可是将主文件变得很小,例如QQ.exe,里面只有程序的运行框架,其余大部分都是dll调用,需要某个功能,将dll加载进来再使用就好了。这样的优点是实现了模块化,dll可以被多个文件所加载;缺点是dll调用会有系统开销
静态链接库(lib)是指在程序运行前加载进程序的库文件,有像我们include<stdio.h>,将stdio.h文件中的所有东西都粘贴到相应位置,静态链接库也一样,在静态链接库里写了什么,那些东西就会粘到我们引用的位置,然后一起编译
dll的一个典型使用就是操作系统,操作系统有多个动态链接库,有控制界面的、控制进程的等等,这些库一般很大。为什么采用动态链接库的形式而不是静态链接库的形式呢?可以想象,如果采用静态链接的方式,所有的程序的指令中都会包含很多相同的内容,而且有很大部分根本用不到,对内存的占用就很大;而采用动态链接库的方式,整个系统中只保留一份,谁要用,就通过指针指向相应函数入口就可以了,有点像C++的引用对象
编写动态链接库
创建dll工程
在VS2019中,文件-创建-项目:
选择动态链接库类型,然后一直下一步即可
创建完成后的项目框架默认如下:
其中,pch是预编译头文件,详细的可以自己去百度一下,总结来说就是将一些不怎么变动的头文件预先编译,加快工程编译速度。
dllmain.cpp是dll程序的主文件
内容如下:
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
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函数是dll的入口点,每次这个dll被加载都会执行DllMain,然后根据运行时状态执行不同的命令,即switch段
编写自己的dll函数
编写函数的方法与在其他C++文件中完全一致,就在dllmain.cpp中编写即可
// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"
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;
}
//编写函数 pch.h里面记得要添加include<iostream>
void test1()
{
std::cout << "test1 is worked\n";
}
void test2()
{
std::cout << "test2 is worked\n";
}
将函数或对象暴露给外界的方法
编写完函数或对象,外界还是无法执行的,这就像js里的模块化编程,你需要将想要给外界使用的功能暴露(export)出来。
暴露的方法有两种
(1) 直接函数名(对象)前加入暴露给外界的关键字
__declspec(dllexport) void test1()
{
std::cout << "test1 is worked\n";
}
这个时候,test1就可以被外界访问到了。
但这里卖个关子,C语言这样写是完全没有问题的,C++在却会因为重载机制无法被外界访问,后面详细解释再给出解决方法
C++中,其实我们更常用的是暴露对象给外界(之后找个时间再写吧)
class __declspec(dllexport) test
{
test() {};
};
(2)使用模块定义文件(.def)
右键项目-添加项-新建项
选择模块定义文件创建
自定义一个名字后在里面写入:
LIBRARY dll测试 ;LIBRAY后面跟dll的项目名称
EXPORTS ;EXPORTS代表后面的都是要export出去的函数
test2 ;一行一个函数名
.def文件中以";"作为注释符
这样写好后保存,test2函数就被暴露给外界了。
生成dll
在vs的顶部工具栏,依次点击生成-生成dll测试,等待即可
找到项目文件夹的debug目录,会发现生成了相应的dll文件
在其他程序中使用DLL
新建一个空项目,编写一个简单的main函数,来练习如何调用加载DLL
//main.cpp : 测试动态链接库
#include <iostream>
typedef void(*func)();
int main(void)
{
//引入要加载的动态链接库
//HMODULE点进去看的话其实是HINSTANCE的一个别名,就是一个句柄
//如果获取到了,会返回这个动态库的句柄,否则返回NULL
HMODULE dlltest = LoadLibraryW(L"dll测试.dll");
if (dlltest)
{
//获取函数名所在的地址,即函数指针
//获取到的地址默认是void类型,因此要自己定义一个函数指针,进行强制类型转换
func test1 = (func)GetProcAddress(dlltest, "test1");
if (test1) {
test1();
}
else {
MessageBoxW(NULL, L"找不到test1方法", L"ERROR", NULL);
}
func test2 = (func)GetProcAddress(dlltest, "test2");
if (test2) {
test2();
}
else {
MessageBoxW(NULL, L"找不到test2方法", L"ERROR", NULL);
}
}
else {
MessageBoxW(NULL,L"找不到dll",L"ERROR",NULL);
}
}
执行后的结果,是先弹出一个提示框,提示”找不到test1方法“,然后出现控制台,显示"test2 is worked"
为什么会这样呢,我们明明export出了test1方法
这要从C++的函数名重载的机制说起
C++支持函数名重载,方法是将原有函数名粉碎,向函数名中添加关于参数的信息,就是说原来的函数名就是test1,但C++会粉碎成?test1@@YAXXZ这样的名字,这也就导致了我们暴露出去的函数名其实根本不是test1
我们可以用VS提供的调试工具,使用dumpbin命令来查看下这个DLL的暴露信息(有时间再更)
看到没有,test2的函数名暴露出来了,但test1的函数名被粉碎成了一串很复杂的名字
我们之前再程序中使用GetProcAddress(HMODULE dll, const char* name) 来获取函数名,name填写的是"test1",函数通过test1这个名字根本找不到,因此name必须填”?test1@@YAXXZ“才行
那么如何解决这个问题呢,只要在编写dll的时候在函数前这样做:
extern "C" __declspec(dllexport) void test1()
{
std::cout << "test1 is worked\n";
}
这样就告诉编译器,用C风格来暴露test函数,就不会被粉碎函数名了
当然,在C++中,我们其实更常用的是对象方式的加载
【有时间再更】