C++动态链接库的使用

news2024/10/6 16:30:59

目录

    • 一 创建Windows动态链接库
    • 二 导出和查看DLL中的函数
    • 三 隐式链接方式加载DLL
    • 四 动态库和测试程序共用一份头文件
    • 五 从DLL中导出C++类
    • 六 解决名称改编
    • 七 显式加载方式加载DLL

一 创建Windows动态链接库

新建项目,选择Windows桌面向导,命名项目名称为Dll1
在这里插入图片描述
应用程序类型中选择动态链接库,勾选空项目。
在这里插入图片描述
项目创建好后,创建一个Dll1.cpp源文件。

二 导出和查看DLL中的函数

  1. 如何导出
    应用程序想要访问某个DLL中的函数,这个函数必须是已经导出的函数。导出需要用以下限定声明。

    __declspec( dllimport ) declarator
    __declspec( dllexport ) declarator

    这两个属性明确定义了DLL与客户端的接口,客户端可以是可执行文件或其他DLL。将函数声明为dllexport之后,就不需要在模块定义(.DEF)文件中导出名称了。

    These attributes explicitly define the DLL’s interface to its client, which can be the executable file or another >DLL. Declaring functions as dllexport eliminates the need for a module-definition (.DEF) file, at least with >respect to the specification of exported functions. Note that dllexport replaces the __export keyword.

    在Dll1.cpp中写入下列代码,注意,这是导出的函数。

_declspec(dllexport) int add(int a, int b)
{
	return a + b;
}

_declspec(dllexport) int subtract(int a, int b)
{
	return a - b;
}

编译,在项目目录下会出现Dll1.lib和Dll1.dll文件。 在这里插入图片描述

  1. 如何查看导出函数
    采用dumpbin,点击【工具】/【Visual Studio命令提示】,即可弹出命令窗口,输入dir命令转到这个Dll文件所在目录。
    在这里插入图片描述
    输入 dumpbin -exports Dll1.dll ,即可显示当前Dll导出的所有函数。
    在这里插入图片描述
  2. lib和Dll的区别
    刚刚编译好的文件中有两个重要的文件.lib和.dll,其中.dll就是动态链接库文件。那么.lib呢?它的名字叫引入库文件,在程序编译链接时,需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才会加载所需的DLL,将该DLL中的函数代码和数据映射到进程的地址空间中,然后访问该DLL中导出的函数。

三 隐式链接方式加载DLL

我们看到Dll1.dll已经导出了两个函数:add和subtract,我们再编一个测试程序测试这个动态链接库。新建一个基于对话框的MFC引用程序,工程名为:DllTest,并在该工程的主对话框上放置两个按钮,给按钮添加响应函数,在响应函数中分别调用前面的add和subtract函数。
为了能正确的调用函数,我们需要做到或者了解以下几件事情。

  1. 声明函数
    我们知道,在C++中,函数必须首先声明才能使用,因此,必须在对话框的源代码中声明这两个函数。这里需要注意,我们最好能够使用标识符_declspec(dllimport)来表明函数是从动态库中引入的。这样,和使用extern或者不加修饰符相比,编译器可以生成运行效率更高的代码。
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b);
  1. 测试程序需要链接正确的引入库文件
    在项目的链接器中选择附加依赖项,填入刚刚生成的引入库文件——Dll1.lib,如下图所示。 在这里插入图片描述
    在常规选项卡中设置附加库目录,把刚才生成Dll1.lib的目录添加进去。在这里插入图片描述
    再次编译,成功生成了TestDll.exe,运行程序,单击按钮可以获得正确的结果。

  2. 查看可执行程序加载的动态库信息
    如果我们想查看某个可执行程序的输入信息,以及其加载的DLL信息,也可以使用dumpbin命令来实现。在命令行方式下,进入DllTest.exe文件所在的目录下,然后输入下述命令并回车

    dumpbin -imports TestDll.exe

    在这里插入图片描述
    这里面显示了所有加载的DLL信息,比如图中红色矩形框内,显示此可执行程序加载了DLL1.dll库,且调用了库里的两个函数:add和subtract

  3. 将动态链接库放在正确的目录下
    上述DllTest.exe程序开始运行时,系统将为它分配一个4GB的地址空间,然后加载模块会分析该应用程序的输入信息,从中找到该程序将要访问的动态链接库信息,然后在用户机器上搜索这些动态链接库,进而加载他们。搜索的顺序是:
    ① 程序的执行目录
    程序的执行目录就是指可执行文件所在的目录。
    ② 当前目录
    一般是指活动应用程序启动的目录,应用程序可以通过调用GetCurrentDirectory函数来确定哪个目录是当前目录;也可以通过SetCurrentDirectory函数来修改当前目录。
    ③ 系统目录
    Windows系统目录指的是操作系统的主要文件存放的目录。
    ④ path环境变量中所列的路径
    “环境变量”是操作系统工作环境设置的一些选项或属性参数。每个环境变量由变量名和文件路径组成的,可以设置很多个环境变量。我们一般使用环境变量指定一个文件夹的位置,或一个应用程序的位置等。而path环境变量只是众多环境变量的其中一个,它的变量名叫做“path”,与其他环境变量没有什么区别,只不过“path”这个环境变量经常用到而已。
    Windows和DOS操作系统中的path环境变量,当要求系统运行一个程序而没有告诉它程序所在的完整路径时,系统除了在当前目录下面寻找此程序外,还应到path中指定的路径去找。用户通过设置环境变量,来更好的运行进程。path环境变量的查看和设置方式如下:通过 右键计算机——>属性——>高级系统设置,进入其中的环境变量即可设置。
    DLL的搜索目录就是以上4个,那么,为了能让测试程序在运行时正确加载DLL,需要将DLL放置于上述任意一个目录内。这里,我们选择放在 TestDll.exe所在的目录下。

四 动态库和测试程序共用一份头文件

上述动态链接库虽然初步得以实现,但是使用起来非常不方便,原因是动态链接库一般都会交给客户程序,但是客户端怎么知道你的动态链接库有什么函数?只能通过一些工具(例如dumpbin)查看导出函数,并猜测函数原型。这种做法对DLL的调用很不方便。
因此,常规的做法是在编写动态链接库时,提供一个头文件,在此文件中提供DLL导出函数原型的声明,以及函数有关的注释文档。
此外,这个头文件也可以由动态链接库程序自身使用,实现接口和实现的分离。
首先,修改DLL1项目,在项目中添加头文件dll1.h,内容如下:

//dll1头文件
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif // DLL1_API

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);
//dll1实现文件
#define DLL1_API _declspec(dllexport)
#include "Dll1.h"

int add(int a, int b)
{
	return a + b;
}

int subtract(int a, int b)
{
	return a - b;
}

程序编译时,头文件不参与编译,源文件单独编译。因此,在编译上述Dll1的cpp文件时,首先定义了DLL1_API宏,将其定义为:_declspec(dllexport)。然后再把头文件包含进来,这将展开头文件,展开之后,首先判断DLL1_API已经定义了,所以不再重新定义该宏,因此add和subtract函数声明的原型都是_declspec(dllexport),表明这两个函数是动态链接库的导出函数。
之后,将这个DLL交由其他程序使用时,只要后者没有定义DLL1_API宏,那么该宏的定义就是:_declspec(dllimport),即add和subtract函数是导入函数。通过上述方法,dll1.h头文件实现了既可以在动态链接库中使用,又可以在客户端使用的目的。
在TestDll项目中,只需要包含此头文件,重新编译运行即可。

五 从DLL中导出C++类

从DLL中导出C++类很简单,只需要在类名称前面加上修饰符_declspec(dllexport)即可。例如,我们在Dll1.h头文件中加入如下代码。

#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif // DLL1_API

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

class DLL1_API Point
{
public:
	void OutPut(int x, int y);
};

实现文件为

#define DLL1_API _declspec(dllexport)
#include "Dll1.h"
#include <Windows.h>
#include <atlstr.h>

int add(int a, int b)
{
	return a + b;
}

int subtract(int a, int b)
{
	return a - b;
}

void Point::OutPut(int x, int y)
{
	HWND hwnd = GetForegroundWindow();
	HDC hdc = GetDC(hwnd);
	CString str;
	str.Format(_T("x=%d,y=%d"), x, y);
	TextOut(hdc, 0, 0, str, wcslen(str));
	ReleaseDC(hwnd, hdc);
}

编译,然后我们查看DLL文件的导出情况。
在这里插入图片描述
发现导出了类Point以及其中成员函数OutPut.
我们在测试程序中新增一个按钮,添加按钮的响应函数,代码为

void CTestDllDlg::OnBnClickedBtnOutput()
{
	// TODO: 在此添加控件通知处理程序代码
	Point pt;
	pt.OutPut(5, 3);
}

编译,运行,算式已经写到了客户区。这表明对DLL中的类成员函数调用成功。
无标题

现在还有个问题,如果只想导出类中的某个函数怎么办?很简单,不要在类名前加_declspec(dllexport)限定符,只在类中特定函数前加即可。

六 解决名称改编

C++编译器在生成DLL时,会对导出的函数进行名称改编,并且不同的编译器使用的改变规则不一样。这样,如果利用不同的编译器分别生成DLL和访问该DLL的客户端程序的话,后者在访问该DLL的导出函数时就会出现问题。例如,如果用C++语言编写一个DLL,那么用C语言编写的客户端程序访问该DLL中的函数时就会出现问题。因为后者将使用函数原始名称来调用DLL中的函数,而C++编译器已经对该名称进行了改编,所以C语言编写的客户端程序就找不到所需的DLL导出函数。
鉴于以上原因,我们希望动态链接库文件在编译时,导出函数的名称不要发生改变。有以下两种方法可以实现。

  1. 增加限定符extern “C”
    具体来说,是在定义导出函数时,加上限定符:extern “C”。但是需要注意,这种方法不能用于导出类和类的成员函数。我们需要注释掉上述Dll1.h和Dll1.cpp中类的代码。然后增加限定符。代码如下
//Dll1.h头文件
#ifdef DLL1_API
#else
#define DLL1_API extern "C" _declspec(dllimport)
#endif // DLL1_API

DLL1_API int add(int a, int b);
DLL1_API int subtract(int a, int b);

/*
class DLL1_API Point
{
public:
	void OutPut(int x, int y);
};
*/
//Dll1.cpp 头文件
#define DLL1_API extern "C" _declspec(dllexport) 
#include "Dll1.h"
#include <Windows.h>
#include <atlstr.h>

int add(int a, int b)
{
	return a + b;
}

int subtract(int a, int b)
{
	return a - b;
}

/*
void Point::OutPut(int x, int y)
{
	HWND hwnd = GetForegroundWindow();
	HDC hdc = GetDC(hwnd);
	CString str;
	str.Format(_T("x=%d,y=%d"), x, y);
	TextOut(hdc, 0, 0, str, wcslen(str));
	ReleaseDC(hwnd, hdc);
}
*/

然后,我们利用dumpbin查看Dll1.dll的导出函数,结果如下。可以看出导出函数的名称均未改编。
在这里插入图片描述

  1. 利用模块定义文件(.def)
    另外一种方法是利用模块定义文件。我们以一个实例进行说明。新建一个动态链接库项目,取名称为Dll2。其头文件为
//Dll2.h
int add(int a, int b);
int subtract(int a, int b);

实现文件为

//Dll2.cpp
#include "Dll2.h"

int add(int a, int b)
{
	return a + b;
}

int subtract(int a, int b)
{
	return a - b;
}

再在工程中添加.def文件(见下面代码),LIBRARY语句用来指定动态链接库的内部名称,该名称一定要与生成动态链接库的名称一致。EXPORTS语句的作用是表明DLL将要导出的函数。当链接器在链接时,会分析这个DEF文件,当发现EXPORTS语句下面有add和subtract这两个符号名,并且他们与源文件中定义的add和subtract函数的名字是一致的时候,它就会以add和subtract两个符号名导出相应的函数。

LIBRARY DLL2
EXPORTS
add
subtract

编译之后,利用dumpbin查看导出函数,名称没有发生改编。
在这里插入图片描述

七 显式加载方式加载DLL

本文之前的测试程序采用的都是隐式加载DLL的方式,具体来说就是程序在编译阶段通过导入库将需要加载的DLL信息保存在程序中,等到程序启动时,加载模块确保所有所需DLL均加载在内存中,并映射到调用进程的地址空间中。如果需要的动态库比较多,资源浪费是比较严重的。
而动态链接的方式仅仅是在调用某个函数时才加载DLL,因此效率更高。使用动态方式加载动态链接库时,需要用到LoadLibrary函数。该函数的作用是将指定的可执行模块映射到调用进程的地址空间。
现在,我们在刚才新建的Dll2项目下新建一个基于对话框的MFC工程,名称为TestDll2,然后在对话框资源编辑器上增加一个按钮,并添加该按钮的消息响应函数。

void CTestDll2Dlg::OnBnClickedBtnAnd()
{
	// TODO: 在此添加控件通知处理程序代码
	HINSTANCE hInst;
	hInst = LoadLibrary(_T("Dll2.dll"));
	typedef int (*ADDPROC)(int a, int b);	//定义函数指针类型
	ADDPROC Add = (ADDPROC)GetProcAddress(hInst, "add");	//获取dll的导出函数
	if (!Add)
	{
		MessageBox(_T("获取函数地址失败"));
		return;
	}
	CString str;
	str.Format(_T("5+3="), Add(5, 3));
	MessageBox(str);
}

TestDll2不用在编译阶段链接Dll2.lib,而是使用了LoadLibrary和GetProcAddress函数。后者可以获取动态链接库中某个函数的地址。这样,就可以通过此地址调用相关的函数了。
这里有三点需要注意:

  1. 及时释放对DLL的引用
    如果采用动态加载方式使用DLL时,在需要访问时,调用LoadLibary函数加载DLL;当不再需要访问该DLL时,调用FreeLibrary函数释放对该DLL的引用。
  2. GetProcAddress的函数名称应该是名称改编后
    在本例中,由于通过.def文件限定了导出名称,因此GetProcAddress函数的第二个参数为"add",但是如果导出名称经过了C++名称改编,这个函数的第二个参数应该是改编后的名称(比如?add@@YAHHH@Z之类的)。
  3. 关于调用约定
    在动态链接中,需要额外注意调用约定。在本例中,Dll2和TestDll2工程没有特别指出调用约定,因此都是__cdecl 。但是如果Dll2工程的函数更改了调用约定(比如,Dll2中函数声明和定义增加了限定符_stdcall),那么访问该DLL的客户端程序也应该采用该约定类型来访问相应的导出函数,否则程序可能异常中断。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/149400.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

GPDB中gp_vmem_protect_limit参数的意义

gp_vmem_protect_limit参数的意义1、gp_vmem_protect_limit参数说明1)在启用了基于资源队列的资源管理系统时&#xff0c;gp_vmem_protect_limit参数表示每个segment分配到的内存大小。预估值计算方式&#xff1a;所有GP数据库进程可用内存大小/发生故障时最大的primary segmen…

最优控制学习笔记2----泛函

文章目录泛函泛函定义泛函的变分自变量的变分泛函相近泛函距离泛函的连续性线性泛函泛函的变分泛函的极值泛函极值的定义泛函的极值泛函极值条件泛函 泛函定义 对于某一类函数集合{x(t)}\{x(t)\}{x(t)} 中的每一个函数 x(t)x(t)x(t), 在映射关系 JJJ 下均有一个确定的数与之对…

100天精通Python(数据分析篇)——第71天:Pandas文本数据处理方法之str/object类型转换、大小写转换、文本对齐、获取长度、出现次数、编码

文章目录每篇前言1. 文本数据类型介绍1&#xff09;类型介绍2&#xff09;类型转换3&#xff09;类型区别区别1&#xff1a;统计字符串时区别2&#xff1a;检查字符串时2. Python字符串内置方法1) 大小写转换2) 文本对齐3&#xff09;获取长度4&#xff09;获取出现次数5&#…

js 右键弹出自定义菜单

演示 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title><style>*{margin…

Node.JS(2)--使用node执行js文件

目录 知识回顾 CommentJS规范 ECMAScript标准规范 模块化 CommonJS规范 模块化 知识回顾 I/O (Input/output) I/O操作指的是对磁盘的读写操作 Node Node是对ES标准一个实现&#xff0c;Node也是一个JS引擎通过Node可以使js代码在服务器端执行Node仅仅对ES标准进行了实…

“程序的编译+链接”,深入认识代码生成可执行程序的过程

目录 引入 编译 预编译 编译 汇编 链接 选项总结 记忆方法 运行环境 引入 博主认为学习本章内容&#xff0c;能够认识在代码跑的时候的过程。 首先&#xff0c;粗略笼统的认识程序运行过程的框架图。 编译 其次&#xff0c;再进行细化&#xff0c;细化编译的过程&…

人工智能轨道交通行业周刊-第29期(2023.1.2-1.8)

本期关键词&#xff1a;站台限界测量机器人、智慧云巴、钢轨伸缩调节器、国铁集团会议、4D毫米波雷达、车道线检测1 整理涉及公众号名单1.1 行业类RT轨道交通中关村轨道交通产业服务平台人民铁道世界轨道交通资讯网铁路信号技术交流北京铁路轨道交通网上榜铁路视点ITS World轨道…

node.js+mysql博客全栈系统源码+数据库,含后台完整基础功能,小程序,web前台站点一键置灰,支持移动端适

一个 "开箱即用" 个人博客全栈系统项目&#xff01;下载地址&#xff1a;node.jsmysql博客全栈系统源码数据库 &#x1f96f; 预览 &#x1f440; 前台预览 &#x1f440; 管理端预览 &#x1f96f; v1.0.2 介绍 芒果’个人博客系统&#xff0c;包括前后台完整基…

Linux下buff/cache占用过大问题

当我们在命令行中执行free -h 查看内存时&#xff0c;发现buff/cache占用过大&#xff0c;导致其他软件没有内存可使用 从图上可以看出&#xff0c;buff/cache占用了1G多。 buff/cache是由于系统读写导致的文件缓存&#xff0c;没有及时释放。 解决方案&#xff1a;清理缓存 …

JWT JWT

JWT&#xff08;JSON WEB TOKEN&#xff09; JWT的组成 header&#xff08;头部&#xff09;&#xff1a;中主要存储了两个字段 alg&#xff0c;typ。 alg表示加密的算法默认&#xff08;HMAC SHA256&#xff09;&#xff0c;typ表示这个令牌的类型默认为JWT。 payload&#…

68、Learning Object-Compositional Neural Radi

简介 设计了一种新的双通路架构&#xff0c;其中场景分支编码场景几何和外观&#xff0c;而对象分支编码以可学习的对象激活码为条件的每个独立对象。为在严重杂乱的场景中生存训练&#xff0c;提出一种场景引导的训练策略&#xff0c;以解决遮挡区域的3D空间模糊性&#xff0c…

【ElasticSearch7.X】学习笔记(四)

【ElasticSearch7.X】学习笔记八、SpringData集成ElasticSearch8.1、框架8.1.1、SpringData8.1.2、Spring Data Elasticsearch8.2、搭建8.2.1、maven引入8.2.2、编写配置8.2.3、编写config8.2.4、Product类8.2.5、dao8.2.6、索引操作8.2.7、文档操作8.2.8、文档搜索八、SpringD…

基于 Tensorflow 2.x 从零训练 15 点人脸关键点检测模型

一、人脸关键点检测数据集 在计算机视觉人脸计算领域&#xff0c;人脸关键点检测是一个十分重要的区域&#xff0c;可以实现例如一些人脸矫正、表情分析、姿态分析、人脸识别、人脸美颜等方向。 人脸关键点数据集通常有 5点、15点、68点、96点、98点、106点、186点 等&#x…

ccc-sklearn-14-朴素贝叶斯(2)

文章目录sklearn中的其他贝叶斯算法一、MultinomialNB多项式贝叶斯sklearn中的MultinomialNB二、BernoulliNB伯努利朴素贝叶斯三、ComplementNB补集朴素贝叶斯案例&#xff1a;贝叶斯做文本分类sklearn中的其他贝叶斯算法 一、MultinomialNB多项式贝叶斯 基于原始的贝叶斯理论…

【openGauss实战2】客户端连接工具及配置

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&#x1f61…

shell第七天练习

awk题目&#xff1a; 1、获取根分区剩余大小 2、获取当前机器ip地址 3、统计出apache的access.log中访问量最多的5个IP 4、打印/etc/passwd中UID大于500的用户名和uid 5、/etc/passwd 中匹配包含root或net或ucp的任意行 7、请打印出/etc/passwd 第一个域&#xff0c;并且在第一…

广度优先搜索BFS进阶(一):多源BFS、优先队列BFS、双端队列BFS

一、多源BFS 在上一篇博客&#xff1a;广度优先搜索BFS基础中&#xff0c;我们接触到的BFS均是单起点&#xff08;单源&#xff09;的&#xff0c;但是对于某一些问题&#xff0c;其有多个起点&#xff0c;此类问题我们称为多源BFS问题。先思考下面一道例题&#xff1a; 1.腐…

类加载,类初始化,对象创建过程总结

总结&#xff1a;假如一个类还未加载到内存中&#xff0c;那么在创建一个该类的实例时&#xff0c;具体过程是怎样的&#xff1f;父类的类构造器<clinit>() -> 子类的类构造器<clinit>() -> 父类的成员变量和实例代码块 -> 父类的构造函数 -> 子类的成…

Go第 11 章 :面向对象编程(下)

Go第 11 章 &#xff1a;面向对象编程(下) 11.1 VSCode 的使用 11.1.1 VSCode 使用技巧和经验 11.2 面向对象编程思想-抽象 11.2.1 抽象的介绍 我们在前面去定义一个结构体时候&#xff0c;实际上就是把一类事物的共有的属性(字段)和行为(方法)提取 出来&#xff0c;形成一…

手把手教你图文并茂windows10安装VMware创建CentOS-7-x86_64运行linux系统

VMware是什么 VMWare (Virtual Machine ware)可以使你的计算机上同时运行几个系统、例如windows、DOS、LINUX等同时存在&#xff0c;可以将这些系统像程序似的随时切换&#xff0c;并且不会影响主系统&#xff0c;所有系统共享一个IP。 下载 VMware官网 安装 网上搜索一个序…