详解dll的封装,以及三种调用方法(将+-×÷封装为例)

奋斗吧
奋斗吧
擅长邻域:未填写

标签: 详解dll的封装,以及三种调用方法(将+-×÷封装为例) Python博客 51CTO博客

2023-07-11 18:24:14 157浏览

详解dll的封装,以及三种调用方法(将+-×÷封装为例),2019.8.15再次编辑,看过《深入理解计算机系统》(吐槽:改名“全面”更好)之后,再回来看这些会豁然开


2019.8.15再次编辑,看过《深入理解计算机系统》(吐槽:改名“全面”更好)之后,再回来看这些会豁然开朗,「编译 --> 链接」的过程平时很少关注(因为用不到,而且涉及很多底层知识、设置、api、编码等乱七八糟不得不用却只用一次的玩意儿),甚至有时别人给的dll配置不上还会让人恼火——其实把程序设计成可链接是为了降低耦合性,而且减少不必要的其他模块的编译工作,便于分工,让每个人的模块各司其职,实际上是很有用的!!!


一、生成DLL

1.新建dll项目

新建 “Visual C++”-“Windows桌面”-“动态链接库(DLL)” 

详解dll的封装,以及三种调用方法(将+-×÷封装为例)_显式

或者

新建 “Visual C++”-“Win32”-“Win32 控制台应用程序” 

再右键 自己的项目:属性,然后在 常规->配置类型 改为动态库(.dll)

详解dll的封装,以及三种调用方法(将+-×÷封装为例)_ide_02

比如我建了一个叫“dllmain”的工程。

 

2.怎么去写代码

 

举个简单例子,做一个能加减乘除+-*/的dll接口:

创建DLL文件目录下现在用得到的有:dllmain.h 以及dllmain.cpp

(新建项目的其他自动生成的文件不要动,不然无法成功生成dll……)

详解dll的封装,以及三种调用方法(将+-×÷封装为例)_显式_03

 

“dllmain.cpp”文件里添加实现功能的代码(加减乘除):

#include "dllmain.h"
#include <stdexcept>  //标准的异常类,除数为0时使用


double Add(double a, double b)
{	return a + b;}

double Subtract(double a, double b)
{	return a - b;}

double Multiply(double a, double b)
{	return a * b;}

double Divide(double a, double b)
{	if (b == 0) throw std::invalid_argument("除数不能是 0!");
	return a / b;
}

 

然后在“dllmain.h”头文件里添加函数声明代码:

这是我们平时的函数声明:

double Add(double a, double b);
double Subtract(double a, double b);
double Multiply(double a, double b);
double Divide(double a, double b);

在dll里为了能被使用,前面添加:extern "C" __declspec(dllexport) 

extern "C" __declspec(dllexport) double Add(double a, double b);
extern "C" __declspec(dllexport) double Subtract(double a, double b);
extern "C" __declspec(dllexport) double Multiply(double a, double b);
extern "C" __declspec(dllexport)  double Divide(double a, double b);

为了高逼格一点,可以使用宏定义(把冗长的"__declspec(dllexport)"换成意思清楚的词)

#define MathFuncDll_API __declspec(dllexport) 

extern "C" MathFuncDll_API double Add(double a, double b);
extern "C" MathFuncDll_API double Subtract(double a, double b);
extern "C" MathFuncDll_API double Multiply(double a, double b);
extern "C" MathFuncDll_API  double Divide(double a, double b);

_declspec(dllexport) 意为指定需要导出的目标(生成dll用的)

extern "C",意为将C语言下的程序导出为DLL(这么做的目的见文末”被其他语言调用注意事项“,一句话就是防止在c++编译后函数入口名与c编译后的不一致导致程序找不到函数报错)

 

在.cpp和.h里添加过代码后,”Ctrl+Shift+B“ 或者 ”生成解决方案“都可以,

生成成功,然后DeBug目录下找到 那个生成的 “dllmain.dll”

 

二、调用(链接)

网上搜了一下,这方面还真是有大不相同的好几种方法。。

动态链接库(Dynamic Link Library)DLL文件与EXE文件一样也是可执行文件,但是DLL也被称之为库,因为里面封装了各种类啊,函数啊之类的东西,就像是一个库一样,存储着很多东西,主要是用来调用的。调用方式主要分为两种:隐式(通过lib文件与头文件) 与 显式(只通过DLL文件)【显式调用又分为:静态(需要lib)、动态】。

方法一:隐式调用(项目设置中调用)

编译程序时需要头文件、lib文件,运行时需要DLL文件,并且运行过程中DLL文件一直被占用。但是可以像正常使用一样直接
用,而不用调用API!

这个时候我们需要三个文件,头文件(.h)、导入库文件(.lib)、动态链接库(.dll)把生成的.dll和.lib两个文件拷入控制台程序(exe)的Debug文件夹下

添加dll、lib、h三者的引用:

DLL 引用方式

右键项目——属性——链接器——常规——附加库目录——添加.dll所在目录【选择你dllmain.dll所在文件夹】

LIB 引用方式

方法1

右键项目——属性——配置属性——VC++目录——库目录:添加.lib所在的文件夹(确认对应libpath的文件夹)

右键项目——属性——链接器——输入——附加依赖项——添加.lib【输入dllmain.lib

方法2

由于有些大工程引用较多,也可以 仅仅右键项目,添加,现有项,把.lib先引用进来。否则会报错“无法打开xxxx.lib”!   

H 引用方式

右键项目——属性——配置属性——VC++目录——包含目录:添加.h所在的文件夹(确认对应需要include的文件夹)

在代码里输入#include"dllmain.h"(见有篇博客说要把extern "C" __declspec(dllexport)前缀删掉 ,因为那是生成dll用的)——添加.h

#include<iostream>
#include"dllmain.h"    //这里引用
using namespace std;

int main()
{
	//隐式加载 dll以及lib,h
	cout << Add(2, 3) << endl;  //5
	cout << Subtract(2, 3) << endl;  //-1
	cout << Multiply(2, 3) << endl;  //6
	cout << Divide(2, 3) << endl;  //0.666
	
	system("pause");//这个例子不加这个会一闪而过
	return 0;
}

发现没有,这跟平时include一个.h的函数一模一样!完全没有别的多余的东西!!!

 

方法二:显式调用(代码中调用)

1.动态显式调用

2.静态显式调用

~~~~~以下是区别对比的几处网摘描述,能大概了解一下区别:

一种是LIB包含了函数所在的DLL文件和文件中函数位置的信息(入口),代码由运行时加载在进程空间中的DLL提供,称为动态链接库dynamic linklibrary。
一种是LIB包含函数代码本身,在编译时直接将代码加入程序当中,称为静态链接库static link library。

共有两种链接方式:
动态链接使用动态链接库,允许可执行模块(.dll文件或.exe文件)仅包含在运行时定位DLL函数的可执行代码所需的信息。
静态链接使用静态链接库,链接器从静态链接库LIB获取所有被引用函数,并将库同代码一起放到可执行文件中。

//    动态,只用DLL
typedef int(_stdcall *pGetMaxN)(int, int);    //定义一个函数指针类型
typedef void(_stdcall *pShowMsg)(char *, char *);
//    静态, 用LIB和DLL
extern "C" __declspec(dllimport) int _stdcall GetMaxNumber(int, int);
extern "C" __declspec(dllimport) void _stdcall ShowMsg(char *, char*);

二.1 动态显式调用

仅仅把生成的dllmain.dll 放到生成的.exe 同一目录下(比如DeBug文件夹)里。

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

	typedef double(*func)(double a, double b);

void main()
{
	//动态加载 dll
	HMODULE hModule = LoadLibrary(L"dllmain.dll");
	if (!hModule)
	{
		cout << "Error!" << endl;
	}
	func Add = func(GetProcAddress(hModule, "Add"));
	func Subtract = func(GetProcAddress(hModule, "Subtract"));
	func Multiply = func(GetProcAddress(hModule, "Multiply"));
	func Divide = func(GetProcAddress(hModule, "Divide"));
	if (Add != NULL)
	{
		cout << Add(2, 3) << endl;  //5
		cout << Subtract(2, 3) << endl;  //-1
		cout << Multiply(2, 3) << endl;  //6
		cout << Divide(2, 3) << endl;  //0.666
	}
	//释放
	FreeLibrary(hModule);
}

其中 L"dllmain.dll"的L是”使用多字节字符集“的意思

也可以”不打L“,然后 右键工程—>属性—>常规—>字符集—>使用多字节字符集。

 

此方法需要函数指针和WIN32 API函数(引入了windows.h)中的LoadLibrary、GetProcAddress装载。

     1、如果在没有导入库文件(.lib),而只有头文件(.h)与动态链接库(.dll)时,我们才需要显式调用,如果这三个文件都全的话,我们就可以使用简单方便的隐式调用。
     2、通常Windows下程序显示调用dll的步骤分为三步(三个函数):LoadLibrary()、GetProcAdress()、FreeLibrary()
 其中,LoadLibrary() 函数用来载入指定的dll文件,加载到调用程序的内存中(DLL没有自己的内存!)
         GetProcAddress() 函数检索指定的动态链接库(DLL)中的输出库函数地址,以备调用
         FreeLibrary() 释放dll所占空间


二.2 静态显式调用

采用lib文件调用DLL(采用Lib文件的调用方式又被称为静态调用)

注意,要把 dllmain.dll 和dllmain.lib放到 生成的.exe 同一目录下(比如DeBug文件夹)(如果不放这里,工程里设置、代码里“#pragma comment(lib,"dllmain.lib的完整路径")”也是可以的)

需要先 右键项目,添加,现有项,把.lib先引用进来。否则会报错“无法打开xxxx.lib”!

#include<iostream>
using namespace std;

#pragma comment(lib,"dllmain.lib")

extern "C" __declspec(dllimport) double  Add(double, double);
extern "C" __declspec(dllimport) double  Subtract(double, double);
extern "C" __declspec(dllimport) double  Multiply(double, double);
extern "C" __declspec(dllimport) double  Divide(double, double);

void main()
{
	//静态加载 dll
		cout << Add(2, 3) << endl;  //5
		cout << Subtract(2, 3) << endl;  //-1
		cout << Multiply(2, 3) << endl;  //6
		cout << Divide(2, 3) << endl;  //0.666	
}

#pragma comment(lib,"dllmain.lib");

表示链接dllmain.lib这个库,和在工程设置里写上链入dllmain.lib的效果一样(两种方式等价,或说一个隐式(工程文件里设置)一个显式(写在代码里)调用)。

对比一下:
调用时:从dll导入的声明【extern "C" __declspec(dllimport) double  Add(double, double);】

生成时:函数导出声明【extern "C" __declspec(dllexport) double Add(double a, double b);】

 

ps:  如果代码输入加了_stdcall,会发生错误

extern "C" __declspec(dllimport) double _stdcall Divide(double, double);

 LNK2019    无法解析的外部符号 __imp__Divide@16,该符号在函数 _main 中被引用 


 

三、extern "C"的原因

关于__stdcall问题,.def文件。这篇博客写的很清楚了

还有我的这篇(c++编译器编译后的函数,为何要加extern"c")

这部分粗略总结就是:

“区别于是否支持重载,函数被c++编译后在库中的名字与c语言不同,比如void f(int x,int y),则c++编译后在库中函数名为_f,c++则为_f_int_int这样的名字。
而extern"C"正是c++提供的解决名字匹配问题的链接符号。”

————当不用extern"C"时,可添加.def文件更名来解决名字匹配问题。

“C和C++的编译器的函数名修饰规则不一致,为了确保导出函数名及入口点函数不变,此时需添加.def文件 ”

“用def文件导出的动态库DLL既可以保证函数名不变也可以保证动态库DLL的入口点函数名不变,同时在.cpp文件中函数定义中加入__stdcall就可以实现导出的DLL被其它语言调用,此时.h头文件的作用仅仅打包给开发者,供其查看导出的函数名及相应参数而已。”

 

 

 

其他参考:

…………………………………………

百度经验(帮助很大!):https://jingyan.baidu.com/article/ff42efa92c49cfc19e2202fd.html

https://jingyan.baidu.com/article/27fa7326e1369346f9271f71.html

https://jingyan.baidu.com/article/3065b3b6a60d88becef8a462.html


…………………………………………

 

四、相关知识:

DLL

作用:函数可执行文件

DLL文件中存放封装的函数和类,当程序需要调用DLL所定义的功能时,需要先载入DLL文件,然后取得函数的地址,最后进行调用。 通过DLL来调用功能,可实现代码的封装与复用,去除功能之间的耦合,有利于模块化。降低应用难度的同时,也可以实现知识产权的保护。 

LIB

作用:二进制函数实现代码或函数在dll文件中的索引地址

lib库有两种:

(1)静态链接库(Static Libary,简称“静态库”)

(2)导入库(Import Libary,简称“导入库”)of 动态连接库(DLL,简称“动态库”)

静态库本身就包含了实际执行代码、符号表等等……而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。导入库文件的作用:告诉链接器调用的函数在哪个DLL中,函数执行代码在DLL中的什么位置。这也就是为什么需要在工程属性的“附加依赖项”中填入.LIB文件,它起到桥梁的作用。如果生成静态库文件,则没有DLL ,只有lib,这时函数可执行代码部分也在lib文件中。

共有两种链接方式:
动态链接使用动态链接库,允许可执行模块(.dll文件或.exe文件)仅包含在运行时定位DLL函数的可执行代码所需的信息。
静态链接使用静态链接库,链接器从静态链接库LIB获取所有被引用函数,并将库同代码一起放到可执行文件中。
 

H头文件

作用:声明函数接口

.h用于编译阶段的审核,比如函数声明的与调用的参数个数、类型是不是对应。

 

dll、lib、h三者关系

 .h头文件是编译时必须的,lib库是链接时需要的,dll动态链接库是运行时需要的。

 

若生成了DLL,则肯定也生成 LIB文件。如果要完成源代码的编译和链接,有头文件和lib就够了。如果也使动态连接的程序运行起来,有dll就够了。在开发和调试阶段,当然最好都有。

综上所述: .h和lib文件是编译器,比如VS2012,在编译的时候调用的,而dll是生成的可执行的文件,比如.exe文件,运行的时候需要调用的。

 

DLL与LIB的区别 :

1.DLL是一个完整程序,其已经经过链接,即不存在同名引用,且有导出表,与导入表lib是一个代码集(也叫函数集)他没有链接,所以lib有冗余,当两个lib相链接时地址会重新建立,当然还有其它相关的不同,用lib.exe就知道了;
2.在生成dll时,经常会生成一个.lib(导入与导出),这个lib实际上不是真正的函数集,其每一个导出导入函数都是跳转指令,直接跳转到DLL中的位置,这个目的是外面的程序调用dll时自动跳转;
3.实际上最常用的lib是由lib.exe把*.obj生成的lib。(引用这里)

 

关于lib和dll的区别如下:
(1)lib是编译时用到的,dll是运行时用到的。如果要完成源代码的编译,只需要lib;如果要使动态链接的程序运行起来,只需要dll。
(2)如果有dll文件,那么lib一般是一些索引信息,记录了dll中函数的入口和位置,dll中是函数的具体内容;如果只有lib文件,那么这个lib文件是静态编译出来的,索引和实现都在其中。使用静态编译的lib文件,在运行程序时不需要再挂动态库,缺点是导致应用程序比较大,而且失去了动态库的灵活性,发布新版本时要发布新的应用程序才行。
(3)动态链接的情况下,有两个文件:一个是LIB文件,一个是DLL文件。LIB包含被DLL导出的函数名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到DLL文件。在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中相应函数代码的地址,从而节省了内存资源。DLL和LIB文件必须随应用程序一起发行,否则应用程序会产生错误。如果不想用lib文件或者没有lib文件,可以用WIN32 API函数LoadLibrary、GetProcAddress装载。

 

代码相关

#pragma once

——意为“只编译一次“

#define MathFuncDll_API __declspec(dllexport) 

——【#define 标识符 字符串】意为 编译时把”MathFuncDll_API“替换为eclspec(dllexport) “(仅仅是字符层面上)

typedef double(*func)(double a, double b);

——为一种数据类型定义一个新名字

 

(1)当“extern”关键字修饰在函数或全局变量的定义中时,表示该函数或全局变量任何文件可以访问,“extern”关键字可以省略不写,缺省下就是”extern”
当“extern”关键字修饰在函数声明或全局变量声明中时,表示限定当前文件只能引用用“extern”关键字修饰定义的函数或全局变量.
(2)当”static”关键字修饰在函数或全局变量的定义中时,表示该函数或全局变量只能由本文件中加了”static”关键字修饰的函数声明或全局变量声明来引用.
当”static”关键字修饰在函数声明或全局变量声明中时,表示限定当前文件只能引用用“static”关键字修饰定义的函数或全局变量.

(3)在CPP源文件的函数和全局变量定义中加了个”C”表示允许C源文件访问该函数和全局变量.如果是C++源文件访它们的话则可加可不加.注意这”C”要大写.

工程相关

在Windows中,定义在dll中的变量、函数和类,如果希望让别的程序能够访问。必须通过manifest文件指定导出目标(变量、函数或类),

或者通过__declspec(dllexport)关键字指定需要导出的目标,然后在使用dll的程序中通过__declspec(dllimport)关键字指定导入的目标。

(摘自:https://jingyan.baidu.com/article/3065b3b6a60d88becef8a462.html

 

应用程序如何找到DLL文件?
使用LoadLibrary显式链接,那么在函数的参数中可以指定DLL文件的完整路径;如果不指定路径,或者进行隐式链接,Windows将遵循下面的搜索顺序来定位DLL(有时游戏提示找不到xxx.dll(多半是微软运行库)放游戏文件夹里就能运行的原因就是这样……)
(1)包含EXE文件的目录
(2)工程目录
(3)Windows系统目录
(4)Windows目录
(5)列在Path环境变量中的一系列目录

 


好博客就要一起分享哦!分享海报

此处可发布评论

评论(0展开评论

暂无评论,快来写一下吧

展开评论

您可能感兴趣的博客

客服QQ 1913284695