头文件、lib 和 dll
三者的作用和联系
① 头文件的作用:声明函数接口
② lib 库有两种,
- 静态链接库(Static Library),索引和实现都在其中
- 动态链接库的导入库,此时 lib 只是一些索引信息,记录了 dll 中函数的入口和位置,dll 中是函数的具体内容
【Note】链接器怎么知道该调用哪个 DLL 文件呢?
这就是导入库文件的作用:告诉链接器调用的函数在哪个 DLL 中,函数执行代码在 DLL中 的什么位置
这也就是为什么需要在工程属性的『附加依赖项』中需要填入 lib 文件,它起到桥梁的作用。如果生成静态链接库文件,则没有 dll,只有 lib,这时函数可执行代码部分也放在 lib 文件中
③ dll 动态链接库的作用:含有函数的可执行代码。有两种加载方式,隐式链接(需要导入库)和显示连接(不需要导入库)
dll 一般会有对应的导入库,方便程序进行隐式链接加载,否则就需要 自己 LoadLibary
调入 dll 文件,然后再调用 GetProcAddress
获得对应函数(划线内容即显示链接加载 dll 方式)。有了导入库,我们只需要链接导入库后按照头文件函数接口的声明调用函数就可以了
④ 总结,
头文件是编译时必须的,lib 库是链接时需要的,dll 动态链接库是运行时需要的
如果要完成源代码的编译,只需要 lib;如果要使动态链接的程序运行起来,只需要 dll
三者的引入
① 添加工程的头文件目录
项目 → 属性 → 配置属性 → C/C++ → 常规 → 附加包含目录:加上头文件的存放目录
② 添加文件引用的 lib 静态库路径
项目 → 属性 → 配置属性 → 链接器 → 常规 → 附加库目录:加上lib文件的存放目录
③ 添加工程引用的 lib 文件名
项目 → 属性 → 配置属性 → 链接器 → 输入 → 附加依赖项:加上lib文件名
【Problem】LINK 2019
出现 LINK 2019 的错误,多半都是 lib 文件引入不正确上面的问题,检查链接器附加库目录和附加以依赖项是否正确配置
④ 添加对应的 dll
这个我建议用 everthing 搜一下当前工程下的 dll 文件都放在哪里,再把我们需要的 dll 文件拷进去就行
生成和使用 dll 文件
我们以一个简单加法例子来说明如何生成和使用 dll 文件,我们先创建一个 DLL 项目,取名作 Add,在该项目下面编写如下两个文件,add.h
文件和 add.cpp
// 下列 ifdef 块是创建使从 DLL 导出更简单的宏的标准方法
// 此 DLL 中的所有文件都是用命令行上定义的 ADD_EXPORTS 符号编译的
// 在使用此 DLL 的任何其他项目上不应定义此符号
// 这样,源文件中包含此文件的任何其他项目都会将
// ADD_API 函数视为是从 DLL 导入的,
// 而此 DLL 则将用此宏定义的符号视为是被导出的
#ifdef ADD_EXPORTS
#define ADD_API __declspec(dllexport)
#else
#define ADD_API __declspec(dllimport)
#endif
extern "C" ADD_API int add(int, int); // 注意这里加上extern "C",不然后面调用会出错
// ADD_API int add(int,int);
注意上面代码中的 extern "C"
,虽然在源代码 add.cpp
中定义的函数名是 add,但在 dll 的二进制文件中,经过编译器的加工,它实际上变成另外一个名称。这个另外的名称在原有函数名 add 的基础上加上了一些函数输入输出参数的信息,这样做主要是为函数重载服务的,所以如果直接这样调用 dll,程序就会一直提示找不到 dll,因为函数名不匹配,解决方法有两种,
- 一是运用
extern "C"
修饰 add,就像我们上面那样 - 二是运用模块定义文件
.def
【Note】为什么要加 extern "C"
详细可以参考这篇博客 C++调用C函数,为什么要加extern “C”?
我们的 add.cpp
文件为
// add.cpp : 定义 DLL 应用程序的导出函数
#define ADD_EXPORTS
#include "add.h"
//#include <stdafx.h>
// 这是导出函数的一个示例
ADD_API int add(int x, int y)
{
int sum = x + y;
return sum;
}
然后不着急点调试项目,在资源管理器中右键我们的 Add 项目,点击生成,如果成功,Add.lib 和 Add.dll 文件就创建好了
【Problem】提示 dll 不是有效的win32应用程序
调试 dll 应该附加到一个 exe 进程中,我们现在还没有 exe 程序,自然会报错
【Problem】在查找预编译头时遇到意外的文件结尾。是否忘记了向源中添加 #include “pch.h”
解决方法 参考这里
隐式调用
接着,我们在同一个解决方案下面新建一个项目 UseAddDll_Implicit,创建的是控制台引用程序,然后在 UseAddDll_Implicit.cpp
文件里编写如下代码
#include <iostream>
#include "add.h"
using namespace std;
int main()
{
int x, y, sum;
cin >> x >> y;
sum = add(x, y);
cout << sum << endl;
}
这时候会发现找不到 add.h
文件,这是正常的,因为我们还没有调用 dll
将 add.h
文件和 Add.lib
、Add.dll
复制到 UseAddDll_Implicit 对应的工程文件夹里面,然后再在工程的属性中添加 lib 文件,不会添加 lib 文件的可以参照下面关于错误的解决方法
【Problem】文件无效或损坏无法在 0x379 处读取
问题发生原因:链接器→输入→附加依赖项,这里面应该是 lib 文件,但错误填写了 dll 文件
显示调用
我们还是在同一个解决方案下面新建一个项目 UseAddDll_Explicit,创建的是控制台引用程序,然后把 Add.dll 文件拷到新项目的文件下,再在 UseAddDll_Explicit.cpp
文件里编写如下代码
#include <Windows.h>
#include <iostream>
// Define a function pointer
typedef int(*FUNA)(int, int);
using namespace std;
int main()
{
const char* dllName = "Add.dll";
const char* funName = "add";
HMODULE hDLL = LoadLibrary(dllName);
if (hDLL != NULL)
{
FUNA fp1 = FUNA(GetProcAddress(hDLL, funName));
if (fp1 != NULL)
{
int x, y, sum;
cin >> x >> y;
sum = fp1(x, y);
cout << sum << endl;
}
else
{
cout << "Func add cannot be found." << endl;
}
FreeLibrary(hDLL);
}
else
{
cout << "Add.dll cannot be loaded." << endl;
}
}
说明两点,
- LoadLibrary、FreeLibrary、GetProcAddress 等都是 Win32 API 函数,包含在头文件 Windows.h 中
- 当程序不再使用 dll 时,应及时调用 FreeLibrary 释放占用的内存空间
【Problem】const char* 类型的实参与 LTCWSTR 类型的形参不兼容
解决方法 参考这里
dll 导出函数名称规范化
我们可以使用 VS 自带的 dumpbin 工具查看 dll
在 VS 界面里面去找工具,然后在命令行里面找到开发者 PowerShell,打开后输入下面命令 dumpbin /exports ./Debug/Add.dll
,接着我们可以下面的内容
当然在前面部分我们已经使用过 extern "C"
来修饰函数,如果不的话,我们将会看到下面这种的函数名
不使用 extern "C"
时 add.h
和 add.cpp
文件编写如下
#ifdef ADD_EXPORTS
#define ADD_API __declspec(dllexport)
#else
#define ADD_API __declspec(dllimport)
#endif
ADD_API int add(int, int); // 注意这里没有使用 extern "C"
// add.cpp : 定义 DLL 应用程序的导出函数
#define ADD_EXPORTS
#include "add.h"
//#include <stdafx.h>
// 这是导出函数的一个示例
ADD_API int add(int x, int y)
{
int sum = x + y;
return sum;
}
这种输出相当不友好,会为 dll 的使用带来很多不便,例如在 LINK2019 的错误出现时,就会出现这种情况。所以,我们希望采用一定的方式使导出的函数修饰名称规范一些
当然 extern "C"
就是一种方法,还有一种是运用模块定义文件 def,这种方法的效果更好
我们在 Add 工程中添加一个 Source.def
文件,文件中编写如下的内容
LIBRARY Add
EXPORTS
add = ?add@@YAHHH@Z
然后再用 dumpbin 查看,结果发现函数名成功的被我们修改了
但是,我们还发现那个奇怪的函数名还是存在,这时可以修改 Add.h
,将 #define ADD_API __declspec(dllexport)
修改为 #define ADD_API
,这时的结果就比较满意了
生成和使用 lib 文件
我们新建一个静态库项目,取名为 AddLib,静态库项目没有 main 函数,也没有像动态链接库中的 dllmain.cpp
文件,创建完项目后自己添加 addlib.h
和 addlib.cpp
文件
#ifndef _MYLIB_H_
#define _MYLIB_H_
extern "C" int add(int, int);
#endif
#include "addlib.h"
#include <iostream>
int add(int x, int y)
{
int sum = x + y;
return sum;
}
调用 lib
调用静态库需要使用头文件和 lib 文件
同样我们还是新建一个工程,然后 将头文件和 lib 文件拷贝到新建的工程目录下面,然后再配置链接器的附加依赖项(或者直接在源代码中加入 #pragma comment(lib, "addlib.lib")
),在源文件中添加 addlib.h
,然后就可以在 UseAddLib.cpp
文件中调用 lib 了
#include "addlib.h"
#include <iostream>
using namespace std;
int main()
{
int x, y, sum;
cin >> x >> y;
sum = add(x, y);
cout << sum;
}
【Note】将现有的项目文件添加到另外一个项目中
我们可以将现有的项目文件拷贝到新的项目文件夹中,然后在 VS 的资源管理器中点击显示所有文件,之后右键点击拷贝过来的文件,选择包含到项目中,这样就好了
我们可以通过次方法将 addlib.h
文件引入到 UseAddLib 工程里面,当然也可以新建一个头文件,然后将代码复制过来
【By the way】哪里去找生成的 lib 文件
生成和使用 exe 文件
如果当前项目调试通过,那么 VS 本身就会在当前工程目录的 Debug 文件夹下生成 exe可执行文件,非常方便
为什么要使用 dll
代码复用是提高软件开发 效率的重要途径。一般而言,只要某部分代码具有通用性,就可将它构造成相对独立的功能模块并在之后的项目中重复使用
『白盒复用』:像是 ATL、MFC等许多应用程序框架,它们都以源代码的形式发布。由于这种复用是源码级别的,源代码完全暴露给了程序员,因而称之为白盒复用
白盒复用的缺点 比较多,总结起来有4点,
- 暴露了源代码
- 容易与程序员的普通代码发生命名冲突
- 多份拷贝,造成存储浪费
- 更新功能模块比较困难
实际上,以上四点概括起来就是,暴露的源代码造成代码严重耦合。为了弥补这些不足,就提出了 『二进制级别』 的代码复用。使用二进制级别的代码复用一定程度上隐藏了源代码,对于缓解代码耦合现象起到了一定的作用。这样的复用被称为 『黑盒复用』
在 Windows 系统中有两种可执行文件,其后缀名分别为 .exe
和 .dll
它们的区别在于,.exe
文件可被独立的装载于内存中运行;而 .dll
文件却不能,它只能被其它进程调用。然而无论什么格式,它们都是二进制文件。上面说到的二进制级别的代码复用,就可以使用 .dll
来实现
与白盒复用相 比,dll 很大程度上弥补了上的几个缺陷,
- dll 是二进制文件,因此隐藏了源代码
- 如果采用显式调用,一般不会发生命名冲突
- 由于 dll 是动态链接到应用程序中去的,它并不会在链接生成程序时原原本本拷贝进去
- 最后,dll 文件相对独立的存在,因此更新功能模块是可行的
当然,实现黑盒复用的途径不只 dll 一种,静态链接库甚至更高级的 COM 组件都是
动态链接库虽然一定程度上实现了黑盒复用,但仍存在着诸多不足,例如
- dll 节省了编译期的时间,但相应延长了运行期的时间,因为在使用 dll 的导出函数时,不但要加载 dll,而且程序将会在模块间跳转,降低了 cache 的命中率
- 若采用隐式调用,仍然需要头文件、lib 文件和 dll 文件三件套,并不能支持完全高效模块的更新
- 显式调用虽然很好地支持模块的更新,但却不能导出类和变量。
- dll 不支持 template
二进制级别的代码复用相比源码级别的复用已经有了很大的进步,但在二进制级别的代码复用中,dll 显得太古老。想真正完美实现跨平台、跨语言的黑盒复用,采用 COM 才是真正正确的选择