第十九讲:动态链接库

写在前面的话:一直对动态链接库不太理解,感觉它很神秘,不知道该怎么使用,通过这个一讲的学习,算是对它有了一个彻底的认识了。

1、什么是动态链接库?

动态链接库是为了实现代码的重用是出现的,它们都是一些独立的文件,其中包含能被可执行程序或其他DLL调用来完成某些工作的函数。动态链接库通常都是不能直接运行的,只有在其他模块调用动态链接库中的函数时,它才发挥作用。

Windows API 的所有函数都包含在DLL中。其中有三个最重要的DLL,Kernal32.dll,它包含用于管理内存、进程和线程的各个函数;User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传递)的各个函数;GDI32.dll,它包含用户画图和显示文本的各个函数。

 2、静态库和动态库的区别

静态库:函数和数据被编译进一个二进制文件(通常扩展名为.lib)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中赋值这些函数和数据并把他们和应用程序的其他模块组合起来创建最终的可执行文件.EXE。

使用时,包含头文件和库文件就可以了。

如果我们使用静态库,那么我们发布我们产品的时候只需要发布产品就可以了,不需要附带静态库。

动态库:在使用动态库的时候,往往提供三个文件:一个后缀名为.h的头文件,一个后缀名同样为.lib的引入库文件和一个DLL动态链接库文件。.h文件包含导出函数的声明,引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要连接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去动态加载DLL,访问DLL中的到处函数。

使用时,有两种方法,见动态链接库加载的两种方式

如果我们在产品开发中使用动态链接库,那么发布产品的时候需要附带发布动态链接库。

注意:静态库.lib 和动态库的引入库.lib文件的区别。

在动态链接库中可以使用别的动态链接库,但是静态库不能使用别的静态库或动态库的函数。

动态链接库中的函数只有导出的函数才能被调用。必须在需要导出的函数前面添加关键字:_declspec(dllexport)

3、示例DLL

不同于C函数,C++支持的函数重载及类成员函数,所以C++必须采用一种技术来区分不同格式的同名函数,这种技术即“名字重组”。值得注意的是:各种编译器的重组风格可能不一致,也就是说,Visual C++和GCC生成的函数模块名可能不一样。

下面我们建立一个DLL示例TestDll

TestDll.cpp代码:

 

1 #include "stdafx.h"
2 
3 _declspec(dllexport) int add(int a, int b)
4 {
5     return a + b;
6 }

 

 

编译项目可以看到在Debug目录下生成了TestDll.dll文件。

我们可以使用dumpbin来查看dll文件中的内容。

可以看到它导出了一个函数名字是:?add@@YAHHH@Z,这就是前面说的C++对函数名的重组,

如果要让C代码可以使用该函数,可以在导出函数前面添加extend "C"修饰(注意:C要大写),这就就不会对名字进行重组了。修改后的代码如下:

1 extern "C"_declspec(dllexport) int add(int a, int b)
2 {
3     return a + b;
4 }

 

 

使用dumpbin来查看dll

 

4、动态链接库加载的两种方式

隐式链接:

 需要使用到3个文件,.h文件、.lib文件、.dll文件。

首先需要包含头文件和lib文件,接着需要把dll文件放在程序可以找到的位置,比如:项目当前目录下。

这样就可以在我们自己的项目中使用导入的动态链接库中定义的函数了。

代码如下:

 1 #include <iostream>
 2 #include <windows.h>
 3 #include "TestDll.h"
 4 
 5 using namespace std;
 6 
 7 #pragma comment(lib, "TestDll.lib")
 8 
 9 int main(void)
10 {
11     cout << add(2, 3);
12     
13     return 0;
14 }

 

动态加载:

需要一个.dll文件即可。

需要使用到函数LoadLbirary、GetProcAddress、FreeLibrary。

LoadLibrary函数加载动态链接库,具体查询MSDN

GetProcAddress函数取得函数地址,具体查询MSDN

FreeLibrary函数释放不用的动态链接库,具体查询MSDN

首先需要使用LoadLibrary函数加载动态链接库。

之后需要使用GetProcAddress函数取得要使用的函数的地址。

最后就可以使用取得的地址来调用函数了。

代码如下:

 

 1 #include <iostream>
 2 #include <windows.h>
 3 
 4 using namespace std;
 5 // 定义函数原型
 6 typedef int (*pFunction)(int, int);
 7 
 8 int main(void)
 9 {
10     HINSTANCE hLib;
11     hLib = ::LoadLibrary("TestDll.dll");
12     pFunction add = (pFunction)::GetProcAddress(hLib, "?add@@YAHHH@Z");
13     cout << add(2, 3);
14     
15     ::FreeLibrary(hLib);
16     return 0;
17 }

 

 

 

5、动态链接库两种使用方式的差别?

采用LoadLibrary的方式即显示调用,这种方式的缺陷是很明显的,名字重组带来的麻烦就是:我们很难猜测到准确的函数名,但是另一个事实是,由于C++支持重载,所以对于同名函数,它必须进行名字重组才能区分开编译后的同名重载函数,但是我们应该知道,编译器在编译源码时,它是最清楚类似于add(int, int)这样的函数名会被改编成什么样子。既然如此,VC++就可以简化我们进行DLL调用的形式,这种方式就是隐式调用。当然还有另一种方法,就是模块定义文件的使用。这个有兴趣可以查查资料。

我在看一本把脉VC++的书看到这样的解释,感觉挺有道理的;

当我们使用一个动态链接库时,在项目中包含了它提供的头文件,之后就去编译项目,则会出现:error LINK2019:无法解析的外部符号 。。。的错误,原因很简单,因为没有我们引用的函数的定义。当我们加入了动态链接库生成的导入库后编译将会成功(假设我们还没有将dll文件放在项目可以找到的位置),然而在运行时会出现错误,无法找到依赖的库。之后我们把dll文件放在项目根目录下,再次运行程序,就成功了。这是为什么呢?

其实在生成动态链接库文件的时候生成的.lib导入库文件的内容可能是这样的:

 1 typedef int (*pFunction)(int, int);
 2 int add(int a, int b)
 3 {
 4     HINSTANCE hLib;
 5     hLib = ::LoadLibrary("Dll2.dll");
 6     pFunction add = (pFunction)::GetProcAddress(hLib, "?add@@YAHHH@Z");
 7     int c = add(a, b);
 8     
 9     ::FreeLibrary(hLib);
10     return c;
11 }

 

也就是说,使用隐式链接最终还是调用了LoadLibrary函数,只不过这些不需要我们去写,生成动态链接库的时候生成的.lib会帮我们做,但是其实我们还是间接的使用了LoadLibrary函数。

这样当我们调用add函数时,lib内部的add模块会动态的调用dll中的add模块,至于dll文件叫什么名字,add函数如何定位,都无须程序员的关心,因此编译器比任何人都清楚;这样通过已有函数的调用来实现一个新的同样接口的函数的功能,常常被称为“代理函数”,代理的含义在于:Dll2.lib中的add函数其实什么都没有做,它只是简单的代理了Dll2.dll中的add函数的功能。这也是为什么当我没有添加导入库.lib文件时,会在编译器出现无法找到函数定义的错误,在lib中的定义,只是对dll中相应函数的代理。

它们之间的关系如图:

总之,我们的程序只需要Dll2.lib就可以像使用静态库一样调用DLL了。当然我们需要知道的是,所有核心的逻辑封装在DLL中。.lib去找.dll的事,对程序员来说是透明的,这正是“隐式调用”的由来。

这一讲就结束了,虽然了解了动态链接库和静态链接库的使用,但是还有需要的知识需要去学习,比如COM,继续努力。本文中的内容除了孙鑫老师的视频的讲解,还借鉴了把脉VC++这本书中第14章的内容。

posted @ 2014-05-17 00:10  冷冰若水  阅读(8714)  评论(1编辑  收藏  举报