1. 动态库和导出和导入
1.1 动态库的导出
1. 创建新项目
新建新项目,选择动态链接库(DLL)。
填写项目名称,并选择项目保存的路径,然后点击创建。
创建完成后,会自动生成如下所示文件,可以根据需要自行修改文件名。其中,pch.h和pch.cpp一般是编写DLL函数的头文件和源文件。
同时,编译器还会帮你在属性管理器中做三件事:
- 将配置类型设置为动态库
- 在预处理中添加以你的工程名命名的动态库导出的宏定义,以我的工程名myDLL为例,会自动添加MYDLL.EXPORTS的宏定义,这个宏定义后面会用到。
- 设置预编译头文件pch.h。这个就对应我们上面提到的pch.h和pch.cpp,如果我们不想使用vs给我们提供的pch.h和pch.cpp,可根据需要不使用预编译投或者修改预编译头文件的名字。
2. 编写DLL函数
1.编写pch.h文件
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
// 添加要在此处预编译的标头
#include "framework.h"
#ifdef MYDLL_EXPORTS
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif
extern int DLLAPI g_value;
class DLLAPI SimpleClass {
public:
SimpleClass();
~SimpleClass();
int getValue() const;
};
extern "C"
{
DLLAPI int myAdd(int a, int b);
DLLAPI int myMinus(int a, int b);
DLLAPI int myMultipy(int a, int b);
DLLAPI double myDevide(int a, int b);
}
#endif //PCH_H
pch.h文件中,定义了全局变量、类以及函数。其中,MYDLL_EXPORTS
就是前面所述的宏定义,在定义了MYDLL_EXPORTS
后,就会定义DLLAPI
为__declspec(dllexport)
。__declspec(dllexport)
用于windows的动态库,其作用是声明导出变量、函数、类、对象等供外面调用,省略给出.def文件。
但是__declspec(dllexport)
声明的函数会被转换为另一个名字,这是因为C语言中有函数的重载,而转换为另一个名字可以避免发生函数重载。当函数名被转换后,我们在导入这个DLL库时就无法引用这个函数了。然而,有一个方法可以避免这个事情的发生,这就是extern "C"
的作用,它让编译器使用C方式的函数命名规则,这样,编译这个库后,函数名就不会发生转换。对于类,由于C语言中没有class,所以无需对class加上extern "C"
。
那有人有疑问了,说为什么还要有一个#define DLLAPI __declspec(dllimport)
呢?其实,这个定义加不加对于导出库是没有任何影响的,但是对于导入库有影响。在MSDN文档里面进行了解释,意思是如果不定义#define DLLAPI __declspec(dllimport)
,就不能独自使用全局变量g_value,只能通过调用getValue()
函数来返回g_value
。也就是说,如果DLL库中没有定义全局变量,即使没有定义#define DLLAPI __declspec(dllimport)
,在导入该DLL库时编译也不会出现任何问题;但是一旦定义了全局变量,那导入该DLL库时,就会有两种情况,第一种情况是如果不独自使用该全局变量,编译也不会出现任何问题,通过调用getValue()函数
也能返回正确的g_value
,第二种情况是独自使用该全局变量,比如std::cout << g_value << std::endl;
,那么在编译时就会报错,如下所示:
综上所述,一般在定义DLL的头文件时,需要加上#define DLLAPI __declspec(dllimport)
这句。
2.编写pch.cpp文件
// pch.cpp: 与预编译标头对应的源文件
#include "pch.h"
// 当使用预编译的头时,需要使用此源文件,编译才能成功。
int g_value = 100;
SimpleClass::SimpleClass()
{
}
SimpleClass::~SimpleClass()
{
}
int SimpleClass::getValue() const
{
return g_value;
}
int myAdd(int a, int b) {
return a + b;
}
int myMinus(int a, int b) {
return a - b;
}
int myMultipy(int a, int b) {
return a * b;
}
double myDevide(int a, int b) {
double m = (double)a / b;
return m;
}
3. 生成动态库
点击 生成->生成解决方案 即可,注意这里解决平台是Debug x64,后面调用的时候也必须和这个平台一致,不然会报错。你也可以使用release,只要做到前后一致即可。
生成的myDLL.dll
和myDLL.lib
保存在${projectName}/x64/Debug
目录下,如果你选择的其他release平台或者x86,就保存在相应的目录下。
很多小伙伴会比较疑惑的一点是,为什么我生成的DLL库,但却会伴随着lib文件呢?
其实,lib文件有两个意思,一个是静态库的意思,但在这里是是导入库的意思。二者的使用方式相同,含义完全不同。windows下的vs生成dll的时候会顺带生成lib(导入库),在导入DLL的时候可以显式导入,即指定DLL的名字和DLL里面函数的名字(这样比较麻烦);或者使用导入库辅助,这样就是为什么我们使用DLL的时候要在链接器指定lib(导入库)的原因了。
下面我们来看看如何导入动态库。
1.2 动态库的导入
1. 创建新项目
新建新项目,选择空项目。
填写项目名称,并选择项目保存的路径,然后点击创建。
2. 属性配置和添加DLL库
1.配置属性
- 设置头文件目录
- 设置库目录
- 在链接器中添加导入库lib
2.添加DLL库到当前工作目录下
如果不添加DLL库,就会出现找不到DLL文件的报错。
说白了,上述的步骤是为了让项目可以找到库的头文件和库文件,最简单粗暴的方法是把.h(包含framework.h和pch.h)、.dll和.lib
文件都复制到当前的工作目录下。这样,就无需进行前两项配置,即无需配置头文件目录的属性和库目录的属性了。
3. 编写调用代码
新建源文件,调用库的变量、函数和类。
#include "pch.h"
#include <iostream>
int main()
{
//调用库函数
int a = 1;
int b = 2;
int sum = myAdd(a, b);
std::cout << sum << std::endl; //3
//调用库变量
std::cout << g_value << std::endl; //100
//调用库类
SimpleClass cls;
int val = cls.getValue();
std::cout << val << std::endl; //100
}
此时,需要注意的是,这里的导入DLL的项目中没有预定义MYDLL_EXPORTS
,所以,pch.h中走的是#define DLLAPI __declspec(dllimport)
这条支路,这里编译就可以顺利通过了,否则就会因为独自使用库中的全局变量而报错。
4. 生成可执行文件
点击三角符号进行生成并执行,在终端即可看到执行结果。
此时,在${projectName}/x64/Debug
中即可看到exe文件。
2. 静态库和导出和导入
2.1 静态库的导出
1. 创建新项目
2. 编写LIB函数
1.编写pch.h文件
// pch.h: 这是预编译标头文件。
// 下方列出的文件仅编译一次,提高了将来生成的生成性能。
// 这还将影响 IntelliSense 性能,包括代码完成和许多代码浏览功能。
// 但是,如果此处列出的文件中的任何一个在生成之间有更新,它们全部都将被重新编译。
// 请勿在此处添加要频繁更新的文件,这将使得性能优势无效。
#ifndef PCH_H
#define PCH_H
// 添加要在此处预编译的标头
#include "framework.h"
extern int g_value;
class SimpleClass {
public:
SimpleClass();
~SimpleClass();
int getValue() const;
};
int myAdd(int a, int b);
int myMinus(int a, int b);
int myMultipy(int a, int b);
double myDevide(int a, int b);
#endif //PCH_H
2.编写pch.cpp文件
// pch.cpp: 与预编译标头对应的源文件
#include "pch.h"
// 当使用预编译的头时,需要使用此源文件,编译才能成功。
int g_value = 100;
SimpleClass::SimpleClass()
{
}
SimpleClass::~SimpleClass()
{
}
int SimpleClass::getValue() const
{
return g_value;
}
int myAdd(int a, int b) {
return a + b;
}
int myMinus(int a, int b) {
return a - b;
}
int myMultipy(int a, int b) {
return a * b;
}
double myDevide(int a, int b) {
double m = (double)a / b;
return m;
}
3. 生成静态库
点击 生成->生成解决方案 即可,注意这里解决平台是Debug x64,后面调用的时候也必须和这个平台一致,不然会报错。你也可以使用release,只要做到前后一致即可。
生成的myDLL.lib
保存在${projectName}/x64/Debug
目录下,如果你选择的其他release平台或者x86,就保存在相应的目录下。
注意,这里的myLIB.lib的文件明显比导出动态库中的.lib文件要大,这也说明了.lib文件的两种含义。
2.2 静态库的导入
1. 创建新项目
2. 属性配置
- 设置头文件目录
- 设置库目录
- 在链接器中添加导入库lib
说白了,上述的步骤是为了让项目可以找到库的头文件和库文件,最简单粗暴的方法是把.h(包含framework.h和pch.h)和.lib
文件都复制到当前的工作目录下。这样,就无需进行前两项配置,即无需配置头文件目录的属性和库目录的属性了。
3. 编写调用代码
#include "pch.h"
#include <iostream>
int main()
{
//调用库函数
int a = 1;
int b = 2;
int sum = myAdd(a, b);
std::cout << sum << std::endl; //3
//调用库变量
std::cout << g_value << std::endl; //100
//调用库类
SimpleClass cls;
int val = cls.getValue();
std::cout << val << std::endl; //100
}
4. 生成可执行文件
点击三角符号进行生成并执行,在终端即可看到执行结果。此时,在${projectName}/x64/Debug
中即可看到exe文件。
3. 总结
DLL的导出步骤是:
- 创建DLL项目
- 编写DLL的.h文件和.cpp文件
- 生成DLL
DLL的导入步骤是:
- 创建空项目
- 配置属性和添加DLL库到工程目录
- 编写调用代码
- 生成可执行文件
LIB的导出步骤是:
- 创建LIB项目
- 编写LIB的.h文件和.cpp文件
- 生成LIB
LIB的导入步骤是:
- 创建空项目
- 配置属性
- 编写调用代码
- 生成可执行文件