C++中的链接库是写好的、可复用的代码,以一种可执行代码的二进制形式存在,可以被操作系统载入内存执行,Windows平台下有静态链接库(lib)动态链接库(dll)两种。

静态链接库使用时链接器从其中获取所有被引用的函数,并将库同代码一起放到exe可执行文件中 ,其运行效率高。但对程序的更新部署和发布很不友好:假如一个模块依赖20个模块,当20个模块其中有一个模块需要更新时,需要将所有的模块都找出来重新编译出一个可执行程序才可以更新成功。

动态链接库不仅包含dll文件,也包含一个lib文件,但它的lib文件仅包含在运行时定位dll文件中函数的可执行代码所需的信息。其运行时进行链接,运行时同时存在exe文件和dll文件。相较于静态链接,动态链接在程序更新时只需要更新对应模块的dll文件,无需所有模块重新编译。

1 静态链接库使用

静态链接库的使用需要用到.lib文件及其对应的.h头文件,其中.h文件中包含静态库中可以使用的函数的声明,需要使用include进行包含。include包含头文件有双引号尖括号两种形式,其形式与msvc在编译的时候搜索头文件的顺序有关,其顺序如下:

  • 用双引号(#include “xxx.h”)包含的头文件,先在本地目录下搜索
  • 如果上一步没搜到,或者用尖括号包含的头文件(#include <>),通过 /I 指定的目录搜索
  • 如果上一步骤没搜到,再搜索通过环境变量 INCLUDE 指定的目录

因此,一般将放在项目文件夹中的头文件使用双引号进行包含,外部的头文件使用尖括号进行包含。

静态链接库使用时主要使项目能够获取.h文件和lib文件,其分别对应着设置中的包含目录和库目录。进行包含时包含目录和库目录不仅在VC++下有对应设置,在C++和链接器下还有对应的附加包含目录和附加库目录,其区别主要是编译器寻找的顺序不同而已,先在附加项中进行查找,然后再进行非附加项查找,因此在两个位置进行设置均可生效。另外链接库还需设置附加依赖项,其作用是说明依赖库的名字。

静态链接库的使用步骤总结如下:

  1. 包含目录或者附加包含目录中添加.h文件所在位置,并在需要使用依赖库的文件中使用include包含该头文件;
  2. 库目录或者附加库目录中添加.lib文件所在位置;
  3. 在链接器的附加依赖项位置添加依赖库的名字。

2 动态链接库的创建和使用

2.1 DLL文件创建

VS2015中可以像编写C++工程一样直接编写DLL文件。

2.2.1 设置

使用新建项目时可以正常选择WiiN32项目或者Win32控制台应用程序:

image-20220920143511322

在应用程序设置界面中类型选择DLL,附加选项可以选上导出符号以及预编译头,也可以都不选。其中,导出符号(符号就是程序中定义的变量或者方法等)意为将DLL中的变量或函数等可以在调用DLL的文件中使用,C++中使用关键字__declspec(dllexport)来表示,勾选这个选项后,VS会为我们生成导出变量、函数以及类的示例,因此一般选择勾选;而预编译头即会为我们的程序添加头文件#include “stdafx.h”,其将一些常用的库放到了预编译头,直接生成了pch文件,可以加快编译速度,没有需要不勾选。

image-20220920143954133

2.2 DLL书写

勾选导出符号后,生成的导出示例程序如下,我们只需要按照其格式把我们需要导出的变量和函数等书写即可。在示例中,其将__declspec(dllexport)定义为了DLLDEMO_API,因此对于需要导出的函数等,示例均使用了DLLDEMO_API修饰。

一些DLL的函数使用了extern “C”(或EXTERN_C)进行修饰,其意义为用C规则编译指定的代码,以C语言函数名导出可以保证导出的函数名不变。由于对于不同的编译器,C++的函数重命名规则是不一样的(重载时使用),因此不使用extern "C"可能会导致一个编辑器编译出的文件在另一个编译器不可用。如果对于一个函数不存在重载的现象,则可以不添加extern “C”。另外,当dll的制造者跟dll的使用者采用同样的语言、同样编程环境,那么就不需要考虑函数重命名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 DLLDEMO_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// DLLDEMO_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef DLLDEMO_EXPORTS
#define DLLDEMO_API __declspec(dllexport)
#else
#define DLLDEMO_API __declspec(dllimport)
#endif

// 此类是从 DLLDemo.dll 导出的
class DLLDEMO_API CDLLDemo {
public:
CDLLDemo(void);
// TODO: 在此添加您的方法。
};

extern DLLDEMO_API int nDLLDemo;//extern声明该变量定义在其他位置

DLLDEMO_API int fnDLLDemo(void);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// DLLDemo.cpp : 定义 DLL 应用程序的导出函数。
//

#include "stdafx.h"
#include "DLLDemo.h"


// 这是导出变量的一个示例
DLLDEMO_API int nDLLDemo=0;

// 这是导出函数的一个示例。
DLLDEMO_API int fnDLLDemo(void)
{
return 42;
}

// 这是已导出类的构造函数。
// 有关类定义的信息,请参阅 DLLDemo.h
CDLLDemo::CDLLDemo()
{
return;
}

2.2 调用DLL

动态链接库的调用和静态链接库类似,只不过多了dll文件的载入,因此包含目录、库目录和附加依赖项的设置和静态链接库的步骤一致,而载入dll文件的方式主要有以下几种:

  1. 添加系统环境变量,VS中调试时可以通过 项目->属性->调试->环境 栏目添加.dll文件的path而成功调试,例如path=XXX/opencv/build/x64/vc14/bin/
  2. 把引用的dll放到工程的可执行文件所在的目录下,即直接将.dll文件放入debug目录下或release目录

另外,像OpenCV一样,生成和调用DLL文件有Debug和Release之分,对应的调用程序必须按照对应关系进行调用,即Debug程序调用Debug生成的DLL,Release程序调用Release生成的DLL。但一般来说,编译成Release的dll,Debug的程序是可以调用的。

调用DLL文件时还需要注意VS运行库的选择(MT\MD\MTd\MDd),在使用多线程MT时候要配合静态库来使用,使用多线程MD的时候要配合动态库来使用,而多线程调试 DLL (/MDd)和多线程调试 DLL (/MTd)是Debug版本,后面没有d的是Release版本。因此,调用DLL文件时需要将运行库选择为对应的/MD或/MDd。