概论
先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。在仓库的发展史上经历了“无库-静态链接库-动态链接库”的时代。
静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。静态链接库和动态链接库的另外一个区别在于静态链接库中不能再包含其他的动态链接库或者静态库,而在动态链接库中还可以再包含其他的动态或静态链接库。
对动态链接库,我们还需建立如下概念:
(1)DLL 的编制与具体的编程语言及编译器无关
只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL都可以相互调用。譬如Windows提供的系统DLL(其中包括了Windows的API),在任何开发环境中都能被调用,不在乎其是Visual Basic、Visual C++还是Delphi。
Visual
C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL 包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。
什么是库
库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。
所谓静态、动态是指链接。回顾一下,将一个程序编译成可执行程序的步骤:
静态库
之所以成为【静态库】,是因为在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。因此对应的链接方式称为静态链接。
试想一下,静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定跟.o文件格式相似。其实一个静态库可以简单看成是一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。静态库特点总结:
l 静态库对函数库的链接是放在编译时期完成的。
l 程序在运行时与函数库再无瓜葛,移植方便。
l 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
下面编写一些简单的四则运算C++类,将其编译成静态库给他人用,头文件如下所示:
MyLib.h
#ifndef __MY_LIB__ #define __MY_LIB__ extern "C" int add(int x, int y); #endif
MyLib.cpp
#include "mylib.h" int add(int x, int y) { return x + y; }
Windows下vs使用lib.exe,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索。一般创建静态库的步骤如图所示:
Windows下创建与使用静态库
创建静态库(.lib)
使用代码:
MyLib.h #ifndef __MY_LIB__ #define __MY_LIB__ extern "C" int add(int x,int y); #endif MyLib.cpp #include "mylib.h" int add(int x, int y) { return x + y; }
如果是使用VS命令行生成静态库,也是分两个步骤来生成程序:
l 首先,通过使用带编译器选项 /c 的 Cl.exe 编译代码 (cl
/c MyLib.cpp),创建名为“MyLib.obj”的目标文件。
l 然后,使用库管理器 Lib.exe 链接代码 (lib MyLib.obj),创建静态库MyLib.lib。
当然,我们一般不这么用,使用VS工程设置更方便。创建win32控制台程序时,勾选静态库类型;打开工程“属性面板”è”配置属性”è”常规”,配置类型选择静态库。
图:vs静态库项目属性设置
Build项目即可生成静态库。
使用静态库有3种方法
方法一:
在VS中使用静态库方法:
l 工程“属性面板”è“通用属性”è “框架和引用”è”添加引用”,将显示“添加引用”对话框。 “项目”选项卡列出了当前解决方案中的各个项目以及可以引用的所有库。 在“项目”选项卡中,选择 StaticLibrary。 单击“确定”。
l 添加MyLib.h 头文件目录,必须修改包含目录路径。打开工程“属性面板”è”配置属性”è “C/C++”è” 常规”,在“附加包含目录”属性值中,键入MyLib.h 头文件所在目录的路径或浏览至该目录。
编译运行OK。
如果引用的静态库不是在同一解决方案下的子工程,而是使用第三方提供的静态库lib和头文件,上面的方法设置不了。还有2中方法设置都可行。
方法二:
打开工程“属性面板”è”配置属性”è “链接器”è”命令行”,输入静态库的完整路径即可。
方法三:
l “属性面板”è”配置属性”è “链接器”è”常规”,附加依赖库目录中输入,指向静态库所在目录;
l “属性面板”è”配置属性”è “链接器”è”输入”,附加依赖库中输入静态库名MyLib.lib。
方法四:
在代码中指定使用#pargma
#include "mylib.h" #pragma comment(lib, "..\\Debug\\libtest.lib")<span style="white-space:pre"> </span>//用pargma来指定lib目录 int _tmain(int argc, _TCHAR* argv[]) { printf("haha max number is %d",add(4,2)); }
静态链接库的优点和缺点:
1.代码装载速度快,执行速度略比动态链接库快;
2.只需保证在开发者的计算机中有正确的.LIB文件,在以二进制形式发布程序时不需考虑在用户的计算机上.LIB文件是否存在及版本问题,可避免DLL地狱等问题。
3.使用静态链接生成的可执行文件体积较大,包含相同的公共代码,造成浪费;
动态库
通过上面的介绍发现静态库,容易使用和理解,也达到了代码复用的目的,那为什么还需要动态库呢?
为什么还需要动态库?
代码复用是提高软件开发 效率的重要途径。一般而言,只要某部分代码具有通用性,就可将它构造成相对独立的功能模块并在之后的项目中重复使用。比较常见的例子是各种应用程序框架, 如ATL、MFC等,它们都以源代码的形式发布。由于这种复用是“源码级别”的,源代码完全暴露给了程序员,因而称之为“白盒复用”。“白盒复用”的缺点
比较多,总结起来有4点。
- 暴露了源代码;
- 容易与程序员的“普通”代码发生命名冲突;
- 多份拷贝,造成存储浪费;
- 更新功能模块比较困难。
实际上,以上4点概括起来就是“暴露的源代码”造成“代码严重耦合”。为了弥补这些不足,就提出了“二进制级别”的代码复用。使用二进制级别的代码复用一定程度上隐藏了源代码,对于缓解代码耦合现象起到了一定的作用。这样的复用被称为“黑盒复用”。
l 空间浪费是静态库的一个问题。
l 另一个问题是静态库对程序的更新、部署和发布页会带来麻烦。如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库特点总结:
l 动态库把对一些库函数的链接载入推迟到程序运行的时期。
l 可以实现进程之间的资源共享。(因此动态库也称为共享库)
l 将一些程序升级变得简单。
l 甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
Window与Linux执行文件格式不同,在创建动态库的时候有一些差异。
l 在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。
l Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。
与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。选择工具栏旁的Release即可将DLL编译为Release状态
Windows下创建与使用动态库
创建动态库(.dll)
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: break; case DLL_THREAD_ATTACH: break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: break; } return TRUE; }
每一个DLL必须有一个入口点,这就象我们用C编写的应用程序一样,必须有一个WINMAIN函数一样。在Non-MFC
DLL中DllMain是一个缺省的入口函数,你不需要编写自己的DLL入口函数,用这个缺省的入口函数就能使动态链接库被调用时得到正确的初始化。如果应用程序的DLL需要分配额外的内存或资源时,或者说需要对每个进程或线程初始化和清除操作时,需要在相应的DLL工程的.CPP文件中对DllMain
()函数按照下面的格式书写。
参数中,hMoudle是动态库被调用时所传递来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符); ul_reason_for_call是一个说明动态库被调原因的标志,当进程或线程装入或卸载动态链接库的时候,操作系统调用入口函数,并说明动态链接库被调用的原因,它所有的可能值为:DLL_PROCESS_ATTACH: 进程被调用、DLL_THREAD_ATTACH: 线程被调用、DLL_PROCESS_DETACH: 进程被停止、DLL_THREAD_DETACH: 线程被停止;lpReserved为保留参数。到此为止,DLL的入口函数已经写了,剩下部分的实现也不难,你可以在DLL工程中加入你所想要输出的函数或变量了。
通常在导出函数的声明时需要有_declspec(dllexport)关键字:
MyDll.h
#ifndef __DLL_H__ #define __DLL_H__ extern "C" __declspec(dllexport) int Add(int x, int y); extern "C" __declspec(dllexport) int Max(int a, int b); extern "C" __declspec(dllexport) int Min(int a, int b); #endif
实现MyDll.cpp
#include "stdafx.h" #include "MyDll.h" int Add(int x, int y) { return x + y; } int Max(int x, int y) { return x > y?x:y; } int Min(int x, int y) { return x > y?y:x; }
DLL是包含若干个函数的库文件,应用程序使用DLL中的函数之前,应该先导出这些函数,以便供给应用程序使用。要导出这些函数有两种方法:
1. 在定义函数时使用导出关键字_declspec(dllexport)
2. 是在创建DLL文件时使用模块定义文件.Def
例子:
第一种: extern "C" _declspec(dllexport) int Max(int a, int b);
第二种: LIBRARY MyDll <span style="color:#009900;">//注意这里的MyDll是工程名如果不同则应用程序连接库时会发生连接错误</span> EXPORTS Max @1
使用动态库
方法一:
l 工程“属性面板”è“通用属性”è “框架和引用”è”添加引用”,将显示“添加引用”对话框。“项目”选项卡列出了当前解决方案中的各个项目以及可以引用的所有库。 在“项目”选项卡中,选择 DynamicLibrary。 单击“确定”。
l 添加DynamicMath.h 头文件目录,必须修改包含目录路径。打开工程“属性面板”è”配置属性”è “C/C++”è” 常规”,在“附加包含目录”属性值中,键入DynamicMath.h 头文件所在目录的路径或浏览至该目录。
编译运行OK。
方法二:
l “属性面板”è”配置属性”è “链接器”è”常规”,附加依赖库目录中输入,动态库所在目录;
l “属性面板”è”配置属性”è “链接器”è”输入”,附加依赖库中输入动态库编译出来的DynamicLibrary.lib。
这里可能大家有个疑问,动态库怎么还有一个DynamicLibrary.lib文件?即无论是静态链接库还是动态链接库,最后都有lib文件,那么两者区别是什么呢?其实,两个是完全不一样的东西。
StaticLibrary.lib的大小为190KB,DynamicLibrary.lib的大小为3KB,静态库对应的lib文件叫静态库,动态库对应的lib文件叫【导入库】。实际上静态库本身就包含了实际执行代码、符号表等等,而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
在Windows下隐式调用动态库
隐式链接就是在程序开始执行时就将DLL文件加载到应用程序当中。实现隐式链接很容易,只要将导入函数关键字_declspec
(dllimport)函数名等写到应用程序相应的头文件中就可以了。下面的例子通过隐式链接调用MyDll.dll库中的Min函数
//动态库 #pragma comment(lib, "..\\Debug\\DllTest.lib")<span style="white-space:pre"> <span style="color:#009900;">//指向Dll相应地lib</span> extern "C" int __declspec(dllimport)Min(int x, int y);<span style="white-space:pre"> <span style="color:#009900;">//声明导入函数</span> int _tmain(int argc, _TCHAR* argv[]) { <span style="color:#009900;">//动态库隐式调用</span> printf("haha max number is %d",Min(4,2)); }
在Windows下显式调用动态库
显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态链接库。
typedef int (*DllFunc)(int, int); int _tmain(int argc, _TCHAR* argv[]) { DllFunc dllFunc; HINSTANCE hInstLib = LoadLibrary(L"DllTest.dll"); if (hInstLib == NULL) { FreeLibrary(hInstLib); } dllFunc = (DllFunc)GetProcAddress(hInstLib, "Add"); if (NULL == dllFunc) { FreeLibrary(hInstLib); } printf("a + b = %d\n", dllFunc(5,2)); dllFunc = (DllFunc)GetProcAddress(hInstLib, "Min"); if(NULL == dllFunc) { FreeLibrary(hInstLib); } printf("a > b = %d\n", dllFunc(4,55)); FreeLibrary(hInstLib); getchar(); return 0; }
应用程序必须进行函数调用以在运行时显式加载 DLL。为显式链接到 DLL,应用程序必须:
l 调用 LoadLibrary(或相似的函数)以加载 DLL 和获取模块句柄。
l 调用 GetProcAddress,以获取指向应用程序要调用的每个导出函数的函数指针。由于应用程序是通过指针调用 DLL 的函数,编译器不生成外部引用,故无需与导入库链接。
l 使用完 DLL 后调用 FreeLibrary。
显式调用C++动态库注意点
对C++来说,情况稍微复杂。显式加载一个C++动态库的困难一部分是因为C++的name
mangling;另一部分是因为没有提供一个合适的API来装载类,在C++中,您可能要用到库中的一个类,而这需要创建该类的一个实例,这不容易做到。
name mangling可以通过extern "C"解决。C++有个特定的关键字用来声明采用C
binding的函数:extern "C" 。用 extern "C"声明的函数将使用函数名作符号名,就像C函数一样。因此,只有非成员函数才能被声明为extern
"C",并且不能被重载。尽管限制多多,extern "C"函数还是非常有用,因为它们可以象C函数一样被dlopen动态加载。冠以extern
"C"限定符后,并不意味着函数中无法使用C++代码了,相反,它仍然是一个完全的C++函数,可以使用任何C++特性和各种类型的参数。
另外如何从C++动态库中获取类,附上几篇相关文章,但我并不建议这么做:
l 《LoadLibrary调用DLL中的Class》:http://www.cppblog.com/codejie/archive/2009/09/24/97141.html
l 《C++ dlopen mini HOWTO》:http://blog.csdn.net/denny_233/article/details/7255673
“显式”使用C++动态库中的Class是非常繁琐和危险的事情,因此能用“隐式”就不要用“显式”,能静态就不要用动态。
动态链接库的优点和缺点:
动态链接库虽然一定程度上实现了“黑盒复用”,但仍存在着诸多不足,笔者能够想到的有下面几点。
1. dll节省了编译期的时间,但相应延长了运行期的时间,因为在使用dll的导出函数时,不但要加载dll,而且程序将会在模块间跳转,降低了cache的命中率。
2. 动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在,因此代码体积较小。
3. 不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,带来好处的同时,也会有问题!如经典的DLL
Hell问题。
4. 若采用隐式调用,仍然需要.h、.lib、.dll文件(“三件套”),并不能有效支持模块的更新。
5. 显式调用虽然很好地支持模块的更新,但却不能导出类和变量。
6. dll不支持Template。