前言
关于 __declspec(dllexport)
和 __declspec(dllimport)
这两个关键字在上大学期间就没见过几次面,直到毕业后在公司项目的代码中又遇到过几次,每次也是绕着走,生怕和它产生什么联系,只知道它和动态链接库 DLL
有关,但是当前这个项目中几乎没有用到自己写的动态链接库,所以我也就心安理得的躲了它这么久。
最近看一些开源项目的源码时又发现了这两个关键字,此时凭借自己掌握的知识和学习方法再来看这两个关键字,发现也没有什么值得害怕的地方,其实简单来说就是 __declspec(dllexport)
是用来说明指定类和函数需要从 DLL
中导出的,而 __declspec(dllimport)
是用来说明指定的类和函数是从DLL中导入的。
说明
__declspec(dllexport)
和__declspec(dllimport)
只在 Windows 平台才有,用来说明类或函数的导出和导入。- 在 Linux 平台上源文件中的所有函数都有一个的visibility属性,默认导出。如果要隐藏所有函数导出,则需要在GCC编译指令中加入
-fvisibility=hidden
参数。 - 生成
dll
的同时还会生成对应的lib
文件,一般是一些索引信息,记录了dll
中函数的入口和位置,这在之前还真的不知道,原来一直以为lib
只是静态库文件呢。
疑问
- 为什么要导入导出,直接把代码拿过来一起编译不好吗?
想要一起编译前提是你得有源代码,如果人家就给你一个动态库或者静态库,你想把源代码放到一起编译的愿望根本实现不了。
- 为什么要分为静态库和动态库?搞这么麻烦,还要导入导出。
这具体的就要查查他们两者的优缺点了,每种事务的产生必要有其产生的原因,比如静态库,很可能就是一个程序员今天在A工程写了一个读取文件的类,过一段时间又在B工程写了一个读取文件的类,代码都差不多,不久又在C工程中直接把代码复制过来改一改又写了一份,这时想到干脆了写个“静态库”这种东西吧,相同的代码直接封装到库中,哪个工程需要就直接拿过来编译,也不需要再复制代码了。
又比如动态库,前面的静态库解决了代码重复开发和维护的问题,但是读取文件的静态库中的代码在A、B、C三个工程中都存在一份,导致每个可执行程序都很大,可不可以共用一份呢?结果又发明了动态库,在编译时只指定函数的入口地址,运行时才加载动态库,这样就使得可执行程序体积大大缩小。
以上内容纯粹我个人想像的,真正发明静态库和动态库是由于什么原因,大家可以自行去了解…
- 动态库要比静态库好吗?
个人感觉合适的才是最好的,不存在动态库要比静态库好的说法,最起码不是全都好,动态库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小,但是运行时要去加载库会花费一定的时间,执行速度相对会慢一些,总的来说静态库是牺牲了空间换时间,而动态库是牺牲了时间换空间。
- .h(头文件) .lib(库文件) .dll(动态链接库文件) 之间的联系和区别
.h
文件是编译时需要的, .lib
是链接时需要的, .dll
是运行时需要的。如果有 .dll
文件,那么 .lib
一般是一些索引信息,记录了 .dll
中函数的入口和位置,.dll
中是函数的具体的执行内容。如果只有 .lib
文件,那么这个 .lib
文件是静态编译出来的,索引和实现都在文件中。
DLL的编写与使用
前面说了这么多,其实就是想带大家先了解一下动态链接库 DLL
,接下来开始编写一个DLL并在另一个工程中使用它,前提是你已经会使用开发工具VS,如果不会先查查教程。
测试环境
- VS2013随意版(个人感觉这个版本启动能快一点)
- Win10畅想版(我也不知道啥版本)
编写DLL
编写 DLL
的方法不知一种,这里只简单介绍一种,对于直接写 .def
文件的方法这里不会展开,尽量依靠开发工具一步步向下执行就好,其实当你理解了开发工具的是怎样工作的,一切就没有那么神秘了,有些步骤直接修改配置文件也是可以实现的,只不过开发工具给我们提供了界面,操作起来更加方便了而已,下面我们开始编写:
- 打开VS新建项目,选择Win32项目,项目名称GenDLL,解决方案名称DLLExample,点击确定:
- 直接下一步,应用程序类型选择
DLL
,点击完成:
- 项目会自动创建一个GenDLL.cpp文件,我们在手动创建一个GenDLL.h文件,两个文件中编写如下代码:
1 | // GenDLL.h |
1 | // GenDLL.cpp : 定义 DLL 应用程序的导出函数。 |
这段代码中有一个 TEST_API
是我在头文件中自定义的,当存在GENDLL_EXPORTS
宏时, TEST_API
代表 __declspec(dllexport)
也就是导出函数,当不存在GENDLL_EXPORTS
宏时, TEST_API
代表 __declspec(dllimport)
表示导入函数,而 GENDLL_EXPORTS 这个宏是与项目名相关的,自动生成的宏,在 DLL
项目中存在格式为 “大写项目名_EXPORTS”。
也就是说同一个头文件中计算加法的函数 add
在 GenDLL
这个生成 DLL
的项目中表示导出函数,在其他使用这个 DLL
的项目中表示导入函数。
- 编译看输出发现有GenDLL.lib和GenDLL.dll两个文件:
1 | 1>------ 已启动生成: 项目: GenDLL, 配置: Debug Win32 ------ |
使用DLL
- 在DLLExample这个解决方案下添加一个新项目,命名为UseDLL,然后点击确定:
- 直接下一步,应用程序类型选择“控制台应用程序”,点击完成:
- 在文件UseDLL.cpp文件中引用之前GenDLL项目的头文件,编写使用
add
函数的代码:
1 | // UseDLL.cpp : 定义控制台应用程序的入口点。 |
- 编译代码发现报错,提示有一个无法解析的外部命令:
1 | 1>------ 已启动生成: 项目: UseDLL, 配置: Debug Win32 ------ |
提示这个错误本意就是说链接没有找到函数实现,链接需要什么文件,前面提到需要lib文件,那么我们设置一下,让UseDLL工程能够找到GenDLL.lib文件。
- 打开UseDLL工程的属性,在“配置属性->链接器->输入->附加依赖项”中添加GenDLL.lib:
- 然后在“配置属性->链接器->常规->附加库目录”中添加GenDLL.lib所在路径“../Debug”即可成功编译:
- 直接运行就可以看到调用DLL的结果,因为这两个工程在同一解决方案下,所以最终UseDLL.exe和GenDLL.dll在同一目录下,这样不会报找不到DLL的错误
- 如果是不同的目录就会像下图那样,提示找不到GenDLL.dll,只要把GenDLL.dll复制到和UseDLL.exe相同目录即可:
加载DLL
上面提到当运行程序找不到 DLL
时可以把 DLL
放到可执行程序程序的目录,有时运行大型软件找不到 DLL
时,我们也会下载一个放到System32目录,其实程序在加载 DLL
的时候是会按照一定顺序的,这些目录包括:包含exe文件的目录、进程的当前工作目录、Windows系统目录、Windows目录、Path环境变量中的一系列目录等等,这些目录的搜索顺序还会受到安全 DLL
搜索模式是否启用的影响。
所以说如果不是对DLL
放置的位置有特殊要求,那么直接放在exe文件所在的目录就好了,一般也是会优先搜索的。
总结
- Windows上才有
__declspec(dllexport)
和__declspec(dllimport)
.h
文件是编译时需要的,.lib
是链接时需要的,.dll
是运行时需要的- 程序运行时加载
DLL
一般优先从exe文件的所在目录优先加载