详解dll的封装,以及三种调用方法(将+-×÷封装为例)
标签: 详解dll的封装,以及三种调用方法(将+-×÷封装为例) Python博客 51CTO博客
2023-07-11 18:24:14 157浏览
2019.8.15再次编辑,看过《深入理解计算机系统》(吐槽:改名“全面”更好)之后,再回来看这些会豁然开朗,「编译 --> 链接」的过程平时很少关注(因为用不到,而且涉及很多底层知识、设置、api、编码等乱七八糟不得不用却只用一次的玩意儿),甚至有时别人给的dll配置不上还会让人恼火——其实把程序设计成可链接是为了降低耦合性,而且减少不必要的其他模块的编译工作,便于分工,让每个人的模块各司其职,实际上是很有用的!!!
一、生成DLL
1.新建dll项目
新建 “Visual C++”-“Windows桌面”-“动态链接库(DLL)”
或者:
新建 “Visual C++”-“Win32”-“Win32 控制台应用程序”
再右键 自己的项目:属性,然后在 常规->配置类型 改为动态库(.dll)
比如我建了一个叫“dllmain”的工程。
2.怎么去写代码
举个简单例子,做一个能加减乘除+-*/的dll接口:
创建DLL文件目录下现在用得到的有:dllmain.h 以及dllmain.cpp
(新建项目的其他自动生成的文件不要动,不然无法成功生成dll……)
在“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)展开评论
展开评论