参考文献: 孙鑫. VC++深入详解.修订版[M]. 电子工业出版社, 2012. 第19章
Step#1: 新建动态链接库文件
文件->新建->项目->选择Visual C+±>Windows桌面->动态链接库
名称DLL-1。新建成功之后, 平台设置为Debug x64平台。
默认生成以下文件:
在Dll-1.cpp中输入如下代码:
#include “stdafx.h”
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
点击: 生成→生成解决方案
显示输出结果: 生成:成功1个;
在E:\1-C++\Dll-1\x64\Debug中可以看到生成的Dll-1.dll文件。
应用程序如果想要访问某个DLL中的函数,那么该函数必须是已经被导出的函数,为了查看一个DLL中有哪些导出函数,可以利用Visual Studio提供的命令行工具:Dumpbin来实现,如何实现可参考文献:https://blog.csdn.net/tpz789/article/details/89635334 。
利用Dumpbin命令查看Dll-1.dll的信息,发现没有任何与函数有关的信息,这说明Dll-1.dll中没有导出函数。如下图。
为了让DLL导出一些函数,需要在每一个将要被导出的函数前面添加标识符:_declspec(dllexport)。于是修改dll-1.cpp文件中的代码,在add函数和subtract函数的定义前面加上_declspec(dllexport)。
// Dll-1.cpp : 定义 DLL 应用程序的导出函数。
#include “stdafx.h”
_declspec(dllexport) int add(int a, int b)
{
return a + b;
}
_declspec(dllexport) int subtract(int a, int b)
{
return a - b;
}
重新生成Dll-1动态链接库,输出窗口中显示如下信息:
可以看到,这时又生成了两个新文件,其中Dll-1.lib文件是引入库文件,在文件中保存的是Dll-1.dll中导出的函数和变量的符号名;Dll.exp文件是一个输出库文件,这里,该文件并不重要。
然后,再次利用Dumpbin命令查看Dll-1.dll导出函数的信息,结果如下图所示。
可以看到,这时多了一些输出信息,其中有这么一段信息:
这就是导出函数的信息。具体含义参考 《孙鑫. VC++深入详解.修订版[M]. 电子工业出版社, 2012. 第19章》
Step#2: 测试Dll-1.dll
新建一个空项目:文件->新建->项目
名称:Dll-1-test
平台选择Debug x64
新建一个源文件Dll-1-test.cpp,在其中输入如下代码:
#include
#include
using namespace std;
extern int add(int a, int b); //利用extern声明外部函数;
extern int subtract(int a, int b); //利用extern声明外部函数;
void main() {
int x = 3;
int y = 6;
int m = add(x, y);
int n = subtract(x, y);
cout << “m:” << m << endl;
cout << “n:” << n << endl;
system(“PAUSE”);
}
生成→生成解决方案
失败1个,这时会出现三个错误:
可以看到,Dll-1-test程序编译成功通过,产生的三个错误是在程序链接时发生的。因为这里调用的add和subtract函数都已经作了声明,所以编译可以通过。当链接时,链接器需要知道这两个函数到底是在哪个地方实现的,它要找到这两个函数的实现。正因为没有找到该信息,所以链接时就出错了。
为了解决这个问题,就需要利用动态链接库的引入库文件了。在Dll-1.dll文件所在目录下,复制Dll-1.lib文件到Dll-1-test程序所在目录下,这个文件中就包含了Dll-1.dll中导出函数的符号名。然后在Dll-1程序中进行链接器的属性配置。在Debug|64文件夹下,双击Microsoft.Cpp.x64.user,在链接器→常规→附加库目录中输入Dll-1.lib的路径:E:\1-C++\Dll-1-test\Dll-1-test,在链接器→输入→附加依赖项中输入Dll-1.lib
重新生成解决方案:
这时会成功生成Dll-1.exe文件。也就是说,当应用程序需要调用某个动态链接库提供的函数时,在程序链接时只需要包含该动态链接库提供的输入库文件就可以了。
此时也可以利用Dumpbin命令查看可执行程序的输入信息,参考文献:https://blog.csdn.net/tpz789/article/details/89635968
回到E:\1-C++\Dll-1-test\x64\Debug,可以看到生成的Dll-1.exe文件,
双击运行。
弹出错误信息,提示在指定的路径下无法找到动态链接库Dll-1.dll文件。
动态链接库的搜索顺序为:
①程序的执行目录:
本例子的执行目录为:E:\1-C++\Dll-1-test\x64\Debug
②当前目录
例如将Dll-1-test.exe拷到一个新创建的文件夹中,这个新文件夹就是当前目录。
③系统目录
依次是:C:\Windows\System32、C:\Windows\SysWOW64
④path环境变量中所列出的路径
本例中将Dll-1.dll文件复制到执行目录,双击Dll-1-test.exe,执行成功。
Step#3: 利用_declspec(dllimport)声明外部函数
除了使用extern关键字表明函数时外部定义的之外,还可以使用标识符:_declspec(dllimport)来表明函数是从动态链接库中引入的。在Dll-1-test程序中,将extern的两个函数注释起来,然后在其后添加如下代码:
_declspec(dllimport) int add(int a, int b);//表明函数是从动态链接库中引入的
_declspec(dllimport) int subtract(int a, int b);//表明函数是从动态链接库中引入的
重新Build,最后发现程序运行时一样的。
如果调用的函数来自于动态链接库,则与使用extern关键字相比,使用_declspec(dllimport)标识符声明外部函数时,编译器可以生产运行效率更高的代码。
Step#4: 完善Dll-1,利用头文件
对上述Dll-1-test的例子来说,因为该程序使用的动态链接库Dll-1.dll是我们自己编写的,所以我们清楚该Dll-1.dll中的导出函数。如果DLL程序的实现者和使用者不是同一个人,那么后者只能通过一些工具来查看该DLL提供的导出函数,并猜测这些函数的原型。这种做法对DLL的调用不是很方便。通常在编写动态链接库时,都会提供一个头文件,在此文件中提供DLL导出函数原型的声明,以及函数的有关注释文档。
接下来,我们就为Dll-1工程添加一个头文件:Dll-1.h,并在其中添加如下的代码:
_declspec(dllimport) int add(int a, int b);
_declspec(dllimport) int subtract(int a, int b)
下面,对Dll-1.h进行改造,使其不仅能够为调用动态链接库的客户端程序服务,同时也能够由动态链接库程序本身来使用。改造后的Dll-1.h文件内容如下所示:
#ifdef Dll_1_API
#else
#define Dll_1_API _declspec(dllimport)
#endif
Dll_1_API int add(int a, int b);
Dll_1_API int subtract(int a, int b);
在该文件中,首先使用条件编译指令判断是否定义了DLL-1-API符号,如果已经定义了该符号,则不作任何处理;否则定义该符号,将其定义为:_declspec(dllimport)。然后使用所定义的DLL-1-API宏来代替add函数和subtract函数声明前面的_declspec(dllimport)标识符。
接下来,在动态链接库的源程序:Dll-1.cpp文件中,首先利用#define指令定义DLL-1-API宏,然后利用#include指令包含Dll-1.h头文件。之后,在定义add和subtract函数时不再需要指定declspec(dllexport)标识符了,所以将其删除,这时的Dll-1.h.cpp文件内容如下所示:
#include “stdafx.h”
#define Dll_1_API _declspec(dllexport)
#include “Dll-1.h”
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
在程序编译时,头文件不参与编译,源文件单独编译。因此,在编译Dll-1.cpp文件时,首先定义DLL-1-API宏,将其定义为:_declspec(dllexport)。然后包含Dll-1.h这一头文件,这将展开该头文件。展开之后,首先判断DLL-1-API是否已经定义了。因为这时已经定义了这个宏,所以不再定义该宏,直接编译其后的add和subtract函数的声明。因为在声明这两个函数时,都使用了DLL-1-API宏,而且这时该宏的定义是:_declspec(dllexport),表明这两个函数是动态链接库的导出函数。
之后,将这个DLL交由其他程序使用时,只要后者没有定义DLL-1-API宏,那么该宏的定义就是:_declspec(dllimport),即add函数和subtract函数是导入函数。通过上述方法,Dll-1.h这个头文件既可以由实现DLL的程序使用,也可以由调用该DLL的客户端程序使用。
重新生成Dll-1.lib和Dll-1.dll文件,将Dll-1.dll复制到E:\1-C++\Dll-1-test\x64\Debug文件夹下,将Dll-1.lib和Dll-1.h复制到工程根目录下E:\1-C++\Dll-1-test\Dll-1-test,另外再将通用属性配置一下,即附加库目录和附加依赖项。
修改Dll-1-test.cpp,代码如下:
#include
#include
#include “Dll-1.h”
using namespace std;
void main() {
int x = 3;
int y = 6;
int m = add(x, y);
int n = subtract(x, y);
cout << “m:” << m << endl;
cout << “n:” << n << endl;
system(“PAUSE”);
}
运行成功,结果也是正确的。
Step#5: 从DLL中导出C++类
在一个动态链接库中还可以导出一个C++类。为了实现这样的功能,仍以Dll-1为例,首先打开Dll-1工程,然后在Dll-1.h文件中添加如下代码:
#ifdef Dll_1_API
#else
#define Dll_1_API _declspec(dllimport)
#endif
Dll_1_API int add(int a, int b);
Dll_1_API int subtract(int a, int b);
class Dll_1_API Point {
public:
void output(int x, int y);
};
在Dll-1.cpp中添加如下代码:
#include “stdafx.h”
#include
#define Dll_1_API _declspec(dllexport)
#include “Dll-1.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) {
std::cout << x << y << std::endl;
}
重新生成解决方案,成功。然后将Dll-1.dll复制到E:\1-C++\Dll-1-test\x64\Debug中,
将Dll-1.lib、Dll-1.h复制到E:\1-C++\Dll-1-test\Dll-1-test中,然后配置属性,再将Dll-1.h添加到Dll-1-test项目中(也可以不添加)。
修改Dll-1-test.cpp的代码为:
#include
#include
#include “Dll-1.h”
using namespace std;
void main() {
int x = 3;
int y = 6;
int m = add(x, y);
int n = subtract(x, y);
cout << “m:” << m << endl;
cout << “n:” << n << endl;
Point pt;
pt.output(5, 3);
system(“PAUSE”);
}
此外,也可以从类中导出部分函数,例如:在Dll-1.h中将Point类再添加一个成员函数:test
class Point {
public:
void Dll_1_API output(int x, int y);
void test();
};
在Dll-1.cpp中添加test函数的实现,该函数不作任何处理。
void Point::test()
{
}
然后生成Dll-1.dll,利用Dumpbin命令的exports选项查看最新的Dll-1.dll的导出信息。
可以看到,对Point类来说,Dll-1.dll仅导出了它的Output成员函数。
重新编译运行,最后的结果是一样的。