动态链接库详解【一】

一、分别编译与链接(Linking)

大多数高级语言都支持分别编译,程序员可以显式地把程序划分为独立的模块或文件,然后每个独立部分分别编译。在编译之后,由链接器把这些独立的片段(称为编译单元)“粘接到一起”。(想想这样做有什么好处?)

在C/C++中,这些独立的编译单元包括obj文件(一般的源程序编译而成)、lib文件(静态链接的函数库)、dll文件(动态链接的函数库)等。

静态链接方式:在程序执行之前完成所有的组装工作,生成一个可执行的目标文件(EXE文件)。

动态链接方式:在程序已经为了执行被装入内存之后完成链接工作,并且在内存中一般只保留该编译单元的一份拷贝。

二、静态链接库与动态链接库

先来阐述一下DLL(Dynamic Linkable Library)的概念,你可以简单的把DLL看成一种仓库,它提供给你一些可以直接拿来用的变量、函数或类。

静态链接库与动态链接库都是共享代码的方式,如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终的EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。

采用动态链接库的优点:(1)更加节省内存;(2)DLL文件与EXE文件独立,只要输出接口不变,更换DLL文件不会对EXE文件造成任何影响,因而极大地提高了可维护性和可扩展性。

三、静态链接库的制作

对静态链接库的讲解不是本文的重点,但是在具体讲解DLL之前,通过一个静态链接库的例子可以快速地帮助我们建立“库”的概念。

在VC++6.0中new一个名称为libTest的static library工程,并新建lib.h和lib.cpp两个文件,lib.h和lib.cpp的源代码如下:

//文件:lib.h

#ifndef LIB_H

#define LIB_H

extern "C" int add(int x,int y);   //声明为C编译、连接方式的外部函数

#endif

//文件:lib.cpp

#include "lib.h"

int add(int x,int y)

{

    return x + y;

}

编译这个工程就得到了一个libTest.lib文件,这个文件就是一个函数库,它提供了add的功能。将头文件lib.h和libTest.lib文件提交给用户后,用户就可以直接使用其中的add函数了。常用的标准C库函数(scanf、printf、memcpy、strcpy等)就来自这种静态库。

四、静态链接库的调用

下面来看看怎么使用这个库。在VC中new一个名为libCall的Win32 Console Application工程,并将上面生成的文件lib.h和libTest.lib文件拷贝到libCall的工程子目录下。libCall工程仅包含一个main.cpp文件,它演示了静态链接库的调用方法,其源代码如下:

#include

#include "lib.h"

#pragma comment( lib, "libTest.lib" )   //指定与静态库一起连接

int main()

{

    printf( "2 + 3 = %d", add( 2, 3 ) );

}

静态链接库的调用就是这么简单,或许我们每天都在用,可是我们没有明白这个概念。代码中#pragma comment( lib , "libTest.lib" )的意思是指本文件生成的.obj文件应与libTest.lib一起连接。

如果不用#pragma comment指定,则可以直接在VC++中设置,如图2,依次选择tools、options、directories、library files菜单或选项,填入库文件路径。图2中加圈的部分为我们添加的libTest.lib文件的路径。


这个静态链接库的例子至少让我们明白了库函数是怎么回事,它们是哪来的。我们现在有下列模糊认识了:

(1)库不是个怪物,编写库的程序和编写一般的程序区别不大,只是库不能单独执行;

(2)库提供一些可以给别的程序调用的东东,别的程序要调用它必须以某种方式指明它要调用之。

以上从静态链接库分析而得到的对库的懵懂概念可以直接引申到动态链接库中,动态链接库与静态链接库在编写和调用上的不同体现在库的外部接口定义及调用方式略有差异。

五、认识动态链接库

动态链接是相对于静态链接而言的。所谓静态链接是指把要调用的函数或者过程链接到可执行文件中,成为可执行文件的一部分。换句话说,函数和过程的代码就在程序的exe文件中,该文件包含了运行时所需的全部代码当多个程序都调用相同函数时,内存中就会存在这个函数的多个拷贝,这样就浪费了宝贵的内存资源。而动态链接所调用的函数代码并没有被拷贝到应用程序的可执行文件中去,而是仅仅在其中加入了所调用函数的描述信息(往往是一些重定位信息)。仅当应用程序被装入内存开始运行时,在Windows的管理下,才在应用程序与相应的DLL之间建立链接关系。当要执行所调用DLL中的函数时,根据链接产生的重定位信息,Windows才转去执行DLL中相应的函数代码。一般情况下,如果一个应用程序使用了动态链接库,Win32系统保证内存中只有DLL的一份复制品

动态链接库的两种链接方法:

(1) 装载时动态链接(Load-time Dynamic Linking):这种用法的前提是在编译之前已经明确知道要调用DLL中的哪几个函数,编译时在目标文件中只保留必要的链接信息,而不含DLL函数的代码;当程序执行时,利用链接信息加载DLL函数代码并在内存中将其链接入调用程序的执行空间中,其主要目的是便于代码共享。

(2) 运行时动态链接(Run-time Dynamic Linking):这种方式是指在编译之前并不知道将会调用哪些DLL函数,完全是在运行过程中根据需要决定应调用哪个函数,并用LoadLibrary和GetProcAddress动态获得DLL函数的入口地址。

六、制作一个简单的DLL

第4节给出了以静态链接库方式提供add函数接口的方法,接下来我们来看看怎样用动态链接库实现一个同样功能的add函数。

如图 3,在VC 中new一个Win32 Dynamic-Link Library工程dllTest。注意不要选择MFC AppWizard(dll),因为这两只方式创建出来的动态库格式不一样。

在建立的工程中添加lib.h及lib.cpp文件,源代码如下:

/* 文件名:lib.h*/

#ifndef LIB_H

#define LIB_H

extern "C" int __declspec(dllexport) add(int x, int y);

#endif

/* 文件名:lib.cpp*/

#include "lib.h"

int add(int x, int y)

{

    return x+y;

}

最后点击Build按钮(或菜单项Build> Build dllTest.dll)。创建成功之后在工程所在子目录的Debug子目录下就可以找到dllTest.dll文件。另外我们还可以看到一个dllTest.lib文件,后面我们解释该文件的作用。

分析上述代码,dllTest工程中的lib.cpp文件与第2节静态链接库版本完全相同,不同在于lib.h对函数add的声明前面添加了__declspec(dllexport)语句。这个语句的含义是声明函数add为DLL的导出函数。DLL内的函数分为两种:

(1) DLL导出函数,可供应用程序调用;

(2) DLL内部函数,只能在DLL程序使用,其他应用程序无法调用它们。

DLL中导出函数的声明有两种方式:一种为上面例子中给出的在函数声明中加上__declspec(dllexport),这里不再举例说明;另外一种方式是采用模块定义(.def) 文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息

下面的代码演示了怎样同.def文件将函数add声明为DLL导出函数(需在dllTest工程中添加lib.def文件):

; lib.def : 导出DLL函数

LIBRARY dllTest

EXPORTS

add @ 1

.def文件的规则为:

(1) LIBRARY语句说明.def文件相应的DLL;

(2) EXPORTS语句后列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示要导出函数的序号为n(在进行函数调用时,这个序号将发挥其作用);

(3) .def 文件中的注释由每个注释行开始处的分号 (;) 指定,且注释不能与语句共享一行。

由此可以看出,例子中lib.def文件的含义为生成名为“dllTest”的动态链接库,导出其中的add函数,并指定add函数的序号为1。

七、动态地加载和调用动态库中的函数

在VC中new一个名为dllCall的Win32 Console Application工程,并将上面生成的dllTest.dll文件拷贝到dllCall的工程子目录下。dllCall工程仅包含一个main.cpp文件,其源代码如下::

#include

#include

typedef int(*lpAddFun)(int, int); //定义函数指针类型

int main(int argc, char *argv[])

{

    HINSTANCE hDll; //DLL句柄

    lpAddFun addFun; //函数指针变量

    hDll = LoadLibrary("dllTest.dll");

    if (hDll != NULL)

    {

        addFun = (lpAddFun)GetProcAddress(hDll, "add");

        if (addFun != NULL)

        {

              int result = addFun(2, 3);

              printf("%d", result);

        }

        FreeLibrary(hDll);

    }

     return 0;

}

下面我们来逐一分析上面的程序。

首先,语句typedef int ( * lpAddFun)(int,int)定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpAddFun的实例addFun;

其次,在函数main中定义了一个DLL HINSTANCE句柄实例hDll,通过Win32 API函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll;

再次,在函数main中通过Win32 API函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun。经由函数指针addFun进行了对DLL中add函数的调用;

最后,应用工程使用完DLL后,在函数main中通过Win32 API函数FreeLibrary释放了已经加载的DLL模块。

上面例子中我们看到了由“LoadLibrary-GetProcAddress-FreeLibrary”系统API提供的三位一体“DLL加载-DLL函数地址获取-DLL释放”方式,这种调用方式称为DLL的动态调用方式。动态调用方式的特点是完全由编程者用Windows API 函数加载和卸载 DLL,程序员可以在运行时决定 DLL 文件何时加载或不加载,决定加载哪个 DLL 文件。

八、静态地加载和和调用动态库中的函数

与动态调用方式相对应的就是静态调用方式。静态调用方式的特点是由编译系统完成对DLL的加载和应用程序结束时 DLL 的卸载。当调用某DLL的应用程序结束时,若系统中还有其它程序使用该 DLL,则Windows对DLL的应用记录减1,直到所有使用该DLL的程序都结束时才释放它。静态调用方式简单实用,但不如动态调用方式灵活。

下面我们来看看静态调用的例子,将编译dllTest工程所生成的dllTest.lib和dllTest.dll文件拷入dllCall工程所在的路径,dllCall执行下列代码:

// 导入库dllTest.lib文件中仅仅是关于其对应的DLL文件中函数的重定位信息

#pragma comment(lib,"dllTest.lib")

extern "C" __declspec(dllimport) add(int x,int y);

int main(int argc, char* argv[])

{

    int result = add(2,3);

    printf("%d",result);

    return 0;

}

由上述代码可以看出,静态调用方式的顺利进行需要完成两个动作:

(1)告诉编译器与DLL相对应的.lib导入库文件所在的路径及文件名,#pragma comment(lib,"dllTest.lib")就是起这个作用。

程序员在建立一个DLL文件时,连接器会自动为其生成一个对应的.lib导入库文件,该文件包含了DLL 导出函数的符号名及序号(并不含有实际的代码)。在应用程序里,.lib文件将作为DLL的替代文件参与编译。

(2)声明导入函数,extern "C" __declspec(dllimport) add(int x,int y)语句中的__declspec(dllimport)发挥这个作用。

静态调用方式不再需要使用系统API来加载、卸载DLL以及获取DLL中导出函数的地址。这是因为,当程序员通过静态链接方式编译生成应用程序时,应用程序中调用的与.lib文件中导出符号相匹配的函数符号将进入到生成的EXE 文件中,.lib文件中所包含的与之对应的DLL文件的文件名也被编译器存储在 EXE文件内部。当应用程序运行过程中需要加载DLL文件时,Windows将根据这些信息发现并加载DLL,然后通过符号名实现对DLL 函数的动态链接。这样,EXE将能直接通过函数名调用DLL的输出函数,就象调用程序内部的其他函数一样。这么说来在编译的时候调用动态链接库的工程还是会需要LIb文件的

九、动态链接库的应用举例

1、所有的Windows系统调用(Windows API函数)都是以动态链接库的形式提供的。我们在Windows目录下的system32文件夹中会看到kernel32.dll、user32.dll和gdi32.dll,windows的大多数API都包含在这些DLL中。kernel32.dll中的函数主要处理内存管理和进程调度;user32.dll中的函数主要控制用户界面;gdi32.dll中的函数则负责图形方面的操作。与这些动态库相对应的导入库分别为kernel32.lib、user32.lib和gdi32.lib。

2、软件的自动更新。Windows应用的开发者常常利用动态链接库来分发软件更新。他们生成一个动态库的新版本,然后用户可以下载,并用它替代当前的版本。当然,新、旧版本动态库的输出接口(即导出函数)必须一致。下一次用户运行应用程序时,应用将自动链接和加载新的动态库。

3、软件插件技术。许多Windows应用软件都支持插件扩展方式,如IE浏览器、Photoshop、Office等等。插件在本质上都是动态库。

4、可扩展的Web服务器。

5、每个Windows驱动程序在本质上都是动态链接库。

你可能感兴趣的:(C++)