前言

动态链接库(Dynamic Link Library 或者 Dynamic-link Library,缩写为 DLL),是微软公司在微软Windows操作系统中,实现共享函数库概念的一种方式。这些库函数的扩展名是 ”.dll”、”.ocx”(包含ActiveX控制的库)或者 “.drv”(旧式的系统驱动程序)。
——百度百科


正文

与之对应的在基础那会学过静态链接库.lib,要使用特定的功能,就必须加载这个静态库,这样在编译的时候其实exe就包含了这个lib。
而现在的动态链接库,他只管调用,不去负责连接的过程,要么你写好了不用,他不发生连接,要么写了要用的时候才会去连接。

静态库编译完成后,已经和exe合并,所以这个exe会比较大。
动态库在内存中连接,并没有本质上的合并,相对而言exe比较小,但是计算机如果缺失这个动态库,那么这个exe基本就废了。

windows常见的动态库(.dll)

  1. gdi32.dll 绘图
  2. user32.dll 用户界面有关的函数
  3. kernel32.dll 内存、线程、进程
  4. d3d9x_11.dll 绘图

动态链接库的意义

  • 模块化
  • 方便更新迭代
  • 提高共享率和利用率
  • 节约内存
  • 本地化支持
  • 跨语言编程
  • 解决版本问题
  • 等等诸如此类

歪瓜的事说不得。能做到动态库的瓜也不是一般人了。

动态库的问题

  • 因为动态链接,需要时间
  • 找不到动态库,exe没法跑
  • 因为更新而导致接口或者参数不一样了,那么以前的代码全废了

创建动态库

vs有模板,直接创建动态链接库的项目就行了。
当然是因为第一次,后面自己想怎么来也无所谓。

其中pch.h 和 pch.cpp是用来预编译的。核心文件自然就是dllmain.cpp。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 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就是这个程序的主入口。

我们自定义一个函数

1
2
3
4
5
6
//前缀试了让编译器导出这个函数 _d
//由于是c++环境,编译的函数,因为有重载,所以会带有很多参数
//我们可以让他用c语言风格编译
extern "C" _declspec(dllexport) int Ave(int a, int b){
return (a + b) / 2;
}

其实要考虑东西比较多。
如果定义这块写成这样很麻烦,可以新建一个头文件,把声明写好,这样定义写起来至少看着正常点。

1
2
#pragma once
extern "C" _declspec(dllexport) int Ave(int a, int b);

不过实际用途上,这个头文件还是为了让用你的库的人用的。

还有一种解决办法

模块化文件

1
2
3
4
LIBRARY

EXPORTS
ave

这么写之后,就不用头文件了。

上述情况中,我们先忽视了c++的函数重载。

1
2
3
4
5
6
7
int ave(int a, int b){
return (a + b) / 2;
}

int ave(int a){
return a + a;
}

有一种解决办法就是提前给链接器做好准备
#pragma comment(linker,"/export:ave=?ave@@YAHHH@Z")
只不过这种写法还不如模块化文件,而且不确定会不会有问题

还有一种比较麻烦的就是还带了函数调用约定

1
2
3
4
//1为调用风格   2为导出   3为调用约定
extern "C" _declspec(dllexport) int _stdcall ave(int a){
return a + a;
}

_stdcall在windows api中倒是常见。不过函数在编译后就不会是单纯ave了。
要指定的话#pragma comment(linker,"/export:ave=_ave@4")
当然显得也有些奇怪

模块化文件之所以能够直接用,其实编译器做了处理的

简单回顾几种

1
2
3
4
5
6
7
8
9
10
11
#pragma comment(linker,"/export:ave=_ave@4")
//.....
int ave_1(int a, int b){
return (a + b) / 2;
}
extern "C" int _stdcall ave(int a){
return a + a;
}
_declspec(dllexport) int _stdcall ave_2(int a){
return (a + a) / 2;
}
1
2
3
4
5
//模块化文件
LIBRARY

EXPORTS
ave_1

因为懒得下拆解dll函数的软件,就纯yy了。

一种通过链接器提前导,一种在代码里导出,一种就是模块化文件导出

dll除了导出函数,还能导出变量


链接动态库

首先新建项目,这会空项目问题不大,然后可以放到一个解决方案下。

右击新建的项目,找到生成项目依赖项,选择

打上勾。

然后新建项目里自然要去调用了。

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <windows.h>

int main(){
//加载dll
HMODULE hMod = LoadLibraryA("myDll.dll");
if (hMod){
std::cout << "模块加载成功!\n";
}

return 0;
}

注:此处loaddll的时候,只写了文件名是因为两个项目在一个解决方案里面,所以生成的exe和dll也在一个文件夹,就不用这么麻烦写路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <windows.h>

int main(){
//加载dll
HMODULE hMod = LoadLibraryA("myDll.dll");
if (hMod){
std::cout << "模块加载成功!\n";

FARPROC func = GetProcAddress(hMod, "ave_1");
if (func){
std::cout << "函数加载成功!\n";
func();
}

}

return 0;
}

typedef int (FAR WINAPI *FARPROC)();类似于函数指针。

发现函数输出乱值,其实也不难猜到,因为没有输入参数,但是它又不报错。

这种情况不报错其实不太好,那么FARPROC是函数指针,我们也可以自己定义一个

1
typedef int (*FAVE_1)(int a, int b);

这样他就会提示要输入参数了。

然后随便输入俩

这样就成功了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <windows.h>
typedef int (*FAVE_1)(int a, int b);

int main(){
//加载dll
HMODULE hMod = LoadLibraryA("myDll.dll");
if (hMod){
std::cout << "模块加载成功!\n";

FAVE_1 func = (FAVE_1)GetProcAddress(hMod, "ave_1");
if (func){
std::cout << "函数加载成功!\n";
std::cout << func(200,300);
}

}

return 0;
}

HMODULE hMod = LoadLibraryA("myDll.dll");这一步就是程序跑的时候,把这个库加载到程序的内存中。

FAVE_1 func = (FAVE_1)GetProcAddress(hMod, "ave_1");这一步是为了把函数的地址取出来,用了自定义类型是因为原本的类型不符合我们的需求。同时要注意,如果dll没有导出这个函数,那么根据这个函数名是找不到的

如果不想用这个dll了,就可以使用FreeLibrary(hMod);

再往后,如果我们想调用这个函数

1
2
3
extern "C" int _stdcall ave(int a){
return a + a;
}

首先肯定要自定义类型了,那么关键在于extern "C"_stdcall要不要加的问题
extern "C"其实是告诉编译器怎么编译它,那么编译完之后其实就不用管了。
但是_stdcall不一样,函数调用约定比较麻烦。所以_stdcall是必须的。

1
typedef int(_stdcall *FAVE)(int);

模板的形参名是可以省略的,有印象的话最好。
至于_stdcall,其实在之前写的线程进程的时候,有用到一个宏WINAPI,它本质上就是_stdcall
所以这么写也没问题typedef int(WINAPI *FAVE)(int);

效果也ok的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <windows.h>
typedef int (*FAVE_1)(int a, int b);
typedef int(_stdcall *FAVE)(int);

int main(){
//加载dll
HMODULE hMod = LoadLibraryA("myDll.dll");
if (hMod){
std::cout << "模块加载成功!\n";

FAVE_1 func = (FAVE_1)GetProcAddress(hMod, "ave_1");
FAVE func1 = (FAVE)GetProcAddress(hMod, "ave");
if (func){
std::cout << "函数加载成功!\n";
std::cout << func(200, 300) << std::endl;
std::cout << func1(200);
}

}
FreeLibrary(hMod);

return 0;
}

结语

关于动态库,其实还有挺多可以优化的地方,但现在了解为主吧。