• 网络学院
  • IT资讯
  • 操作系统
  • 网络技术
  • 软件应用
  • 办公软件
  • 编程技术
  • 网站架设
  • 数据库类
  • 平面设计
  • 多媒体类
  • 游戏资讯
  • 教学论文
  • 认证考试
VC++动态链接库(DLL)编程深入浅出(二)
  站点:
  • 首 页
  • 最新软件
  • 文章教程
  • 国内软件
  • 国外软件
  • 绿色软件
  • 源码下载
  • 字体下载
VC++动态链接库(DLL)编程深入浅出(二)
软件发布 VC++动态链接库(DLL)编程深入浅出(二)
网络软件 系统工具 应用软件 联络聊天 图形图像 多媒体类 行业软件 游戏娱乐 编程开发 安全相关 教育教学 数码软件 绿软下载
热门软件: QQ 瑞星 pplive e话通 木马克星 千千静听 office2000 五笔字根 Photoshop 视频分割
返回文章教程首页 >> 文章首页 >> 认证考试 >> 程序员 >> VC++动态链接库(DLL)编程深入浅出(二)

VC++动态链接库(DLL)编程深入浅出(二)

添加时间: 2007-4-8 4:00:43  作者: 程序员认证参考  阅读次数:223   来源: http://www.d9soft.com

       

4.1一个简单的dll

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

  如图6,在vc++中new一个win32 dynamic-link library工程dlltest(单击此处下载本工程附件)。注意不要选择mfc appwizard(dll),因为用mfc appwizard(dll)建立的将是第5、6节要讲述的mfc 动态链接库。

 

 

图6 建立一个非mfc 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;

}

  与第2节对静态链接库的调用相似,我们也建立一个与dll工程处于同一工作区的应用工程dllcall,它调用dll中的函数add,其源代码如下:
#include <stdio.h>

#include <windows.h>

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

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

{

hinstance hdll; //dll句柄

lpaddfun addfun; //函数指针

hdll = loadlibrary("..\debug\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;

}

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

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

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

  而应用程序对本dll的调用和对第2节静态链接库的调用却有较大差异,下面我们来逐一分析。

  首先,语句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模块。

  通过这个简单的例子,我们获知dll定义和调用的一般概念:

  (1)dll中需以某种特定的方式声明导出函数(或变量、类);

  (2)应用工程需以某种特定的方式调用dll的导出函数(或变量、类)。

  下面我们来对“特定的方式进行”阐述。

4.2 声明导出函数

  dll中导出函数的声明有两种方式:一种为4.1节例子中给出的在函数声明中加上__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。

4.3 dll的调用方式

  在4.1节的例子中我们看到了由“loadlibrary-getprocaddress-freelibrary”系统api提供的三位一体“dll加载-dll函数地址获取-dll释放”方式,这种调用方式称为dll的动态调用。

  动态调用方式的特点是完全由编程者用 api 函数加载和卸载 dll,程序员可以决定 dll 文件何时加载或不加载,显式链接在运行时决定加载哪个 dll 文件。

  与动态调用方式相对应的就是静态调用方式,“有动必有静”,这来源于物质世界的对立统一。“动与静”,其对立与统一竟无数次在技术领域里得到验证,譬如静态ip与dhcp、静态路由与动态路由等。从前文我们已经知道,库也分为静态库与动态库dll,而想不到,深入到dll内部,其调用方式也分为静态与动态。“动与静”,无处不在。《周易》已认识到有动必有静的动静平衡观,《易.系辞》曰:“动静有常,刚柔断矣”。哲学意味着一种普遍的真理,因此,我们经常可以在枯燥的技术领域看到哲学的影子。

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

  下面我们来看看静态调用的例子(单击此处下载本工程附件),将编译dlltest工程所生成的.lib和.dll文件拷入dllcall工程所在的路径,dllcall执行下列代码:
#pragma comment(lib,"dlltest.lib")

//.lib文件中仅仅是关于其对应dll文件中函数的重定位信息

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的输出函数,就象调用程序内部的其他函数一样。

4.4 dllmain函数

  windows在加载dll的时候,需要一个入口函数,就如同控制台或dos程序需要main函数、win32程序需要winmain函数一样。在前面的例子中,dll并没有提供dllmain函数,应用工程也能成功引用dll,这是因为windows在找不到dllmain的时候,系统会从其它运行库中引入一个不做任何操作的缺省dllmain函数版本,并不意味着dll可以放弃dllmain函数。

  根据编写规范,windows必须查找并执行dll里的dllmain函数作为加载dll的依据,它使得dll得以保留在内存里。这个函数并不属于导出函数,而是dll的内部函数。这意味着不能直接在应用工程中引用dllmain函数,dllmain是自动被调用的。

  我们来看一个dllmain函数的例子(单击此处下载本工程附件)。
bool apientry dllmain( handle hmodule,

dword ul_reason_for_call,

lpvoid lpreserved

)

{

switch (ul_reason_for_call)

{

case dll_process_attach:

printf(" process attach of dll");

break;

case dll_thread_attach:

printf(" thread attach of dll");

break;

case dll_thread_detach:

printf(" thread detach of dll");

break;

case dll_process_detach:

printf(" process detach of dll");

break;

}

return true;

}

  dllmain函数在dll被加载和卸载时被调用,在单个线程启动和终止时,dllmain函数也被调用,ul_reason_for_call指明了被调用的原因。原因共有4种,即process_attach、process_detach、thread_attach和thread_detach,以switch语句列出。

  来仔细解读一下dllmain的函数头bool apientry dllmain( handle hmodule, word ul_reason_for_call, lpvoid lpreserved )。

  apientry被定义为__stdcall,它意味着这个函数以标准pascal的方式进行调用,也就是winapi方式;

  进程中的每个dll模块被全局唯一的32字节的hinstance句柄标识,只有在特定的进程内部有效,句柄代表了dll模块在进程虚拟空间中的起始地址。在win32中,hinstance和hmodule的值是相同的,这两种类型可以替换使用,这就是函数参数hmodule的来历。

  执行下列代码:
hdll = loadlibrary("..\debug\dlltest.dll");

if (hdll != null)

{

addfun = (lpaddfun)getprocaddress(hdll, makeintresource(1));

//makeintresource直接使用导出文件中的序号

if (addfun != null)

{

int result = addfun(2, 3);

printf(" call add in dll:%d", result);

}

freelibrary(hdll);

}


  我们看到输出顺序为:

  process attach of dll

  call add in dll:5

  process detach of dll

  这一输出顺序验证了dllmain被调用的时机。

  代码中的getprocaddress ( hdll, makeintresource ( 1 ) )值得留意,它直接通过.def文件中为add函数指定的顺序号访问add函数,具体体现在makeintresource ( 1 ),makeintresource是一个通过序号获取函数名的宏,定义为(节选自winuser.h):
#define makeintresourcea(i) (lpstr)((dword)((word)(i)))

#define makeintresourcew(i) (lpwstr)((dword)((word)(i)))

#ifdef unicode

#define makeintresource makeintresourcew

#else

#define makeintresource makeintresourcea

4.5 __stdcall约定


  如果通过vc++编写的dll欲被其他语言编写的程序调用,应将函数的调用方式声明为__stdcall方式,winapi都采用这种方式,而c/c++缺省的调用方式却为__cdecl。__stdcall方式与__cdecl对函数名最终生成符号的方式不同。若采用c编译方式(在c++中需将函数声明为extern "c"),__stdcall调用约定在输出函数名前面加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;而__cdecl调用约定仅在输出函数名前面加下划线,形如_functionname。

  windows编程中常见的几种函数类型声明宏都是与__stdcall和__cdecl有关的(节选自windef.h):
#define callback __stdcall //这就是传说中的回调函数

#define winapi __stdcall //这就是传说中的winapi

#define winapiv __cdecl

#define apientry winapi //dllmain的入口就在这里

#define apiprivate __stdcall

#define pascal __stdcall

  在lib.h中,应这样声明add函数:
int __stdcall add(int x, int y);

  在应用工程中函数指针类型应定义为:
typedef int(__stdcall *lpaddfun)(int, int);

  若在lib.h中将函数声明为__stdcall调用,而应用工程中仍使用typedef int (* lpaddfun)(int,int),运行时将发生错误(因为类型不匹配,在应用工程中仍然是缺省的__cdecl调用),弹出如图7所示的对话框。

图7 调用约定不匹配时的运行错误

  图8中的那段话实际上已经给出了错误的原因,即“this is usually a result of …”。

  单击此处下载__stdcall调用例子工程源代码附件。

4.6 dll导出变量

  dll定义的全局变量可以被调用进程访问;dll也可以访问调用进程的全局数据,我们来看看在应用工程中引用dll中变量的例子(单击此处下载本工程附件)。


/* 文件名:lib.h */

#ifndef lib_h

#define lib_h

extern int dllglobalvar;

#endif


/* 文件名:lib.cpp */

#include "lib.h"

#include <windows.h>


int dllglobalvar;


bool apientry dllmain(handle hmodule, dword ul_reason_for_call, lpvoid lpreserved)

{

switch (ul_reason_for_call)

{

case dll_process_attach:

dllglobalvar = 100; //在dll被加载时,赋全局变量为100

break;

case dll_thread_attach:

case dll_thread_detach:

case dll_process_detach:

break;

}

return true;

}


;文件名:lib.def

;在dll中导出变量

library "dlltest"

exports

dllglobalvar constant

;或dllglobalvar data

getglobalvar

  从lib.h和lib.cpp中可以看出,全局变量在dll中的定义和使用方法与一般的程序设计是一样的。若要导出某全局变量,我们需要在.def文件的exports后添加:
变量名 constant   //过时的方法

  或
变量名 data     //vc++提示的新方法

  在主函数中引用dll中定义的全局变量:
#include <stdio.h>

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

extern int dllglobalvar;

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

{

printf("%d ", *(int*)dllglobalvar);

*(int*)dllglobalvar = 1;

printf("%d ", *(int*)dllglobalvar);


return 0;

}

  特别要注意的是用extern int dllglobalvar声明所导入的并不是dll中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用dll中的全局变量。这一点,从*(int*)dllglobalvar可以看出。因此在采用这种方式引用dll全局变量时,千万不要进行这样的赋值操作:
dllglobalvar = 1;

  其结果是dllglobalvar指针的内容发生变化,程序中以后再也引用不到dll中的全局变量了。

  在应用工程中引用dll中全局变量的一个更好方法是:
#include <stdio.h>

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

extern int _declspec(dllimport) dllglobalvar; //用_declspec(dllimport)导入

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

{

printf("%d ", dllglobalvar);

dllglobalvar = 1; //这里就可以直接使用, 无须进行强制指针转换

printf("%d ", dllglobalvar);

return 0;

}

  通过_declspec(dllimport)方式导入的就是dll中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。

4.7 dll导出类

  dll中定义的类可以在应用工程中使用。

  下面的例子里,我们在dll中定义了point和circle两个类,并在应用工程中引用了它们(单击此处下载本工程附件)。
//文件名:point.h,point类的声明

#ifndef point_h

#define point_h

#ifdef dll_file

class _declspec(dllexport) point //导出类point

#else

class _declspec(dllimport) point //导入类point

#endif

{

public:

float y;

float x;

point();

point(float x_coordinate, float y_coordinate);

};

#endif


//文件名:point.cpp,point类的实现

#ifndef dll_file

#define dll_file

#endif

#include "point.h"

//类point的缺省构造函数

point::point()

{

x = 0.0;

y = 0.0;

}

//类point的构造函数

point::point(float x_coordinate, float y_coordinate)

{

x = x_coordinate;

y = y_coordinate;

}


//文件名:circle.h,circle类的声明

#ifndef circle_h

#define circle_h

#include "point.h"

#ifdef dll_file

class _declspec(dllexport)circle //导出类circle

#else

class _declspec(dllimport)circle //导入类circle

#endif

{

public:

void setcentre(const point ¢repoint);

void setradius(float r);

float getgirth();

float getarea();

circle();

private:

float radius;

point centre;

};

#endif


//文件名:circle.cpp,circle类的实现

#ifndef dll_file

#define dll_file

#endif

#include "circle.h"

#define pi 3.1415926

//circle类的构造函数

circle::circle()

{

centre = point(0, 0);

radius = 0;

}

//得到圆的面积

float circle::getarea()

{

return pi *radius * radius;

}

//得到圆的周长

float circle::getgirth()

{

return 2 *pi * radius;

}

//设置圆心坐标

void circle::setcentre(const point ¢repoint)

{

centre = centrepoint;

}

//设置圆的半径

void circle::setradius(float r)

{

radius = r;

}

  类的引用:
#include "..circle.h"  //包含类声明头文件

#pragma comment(lib,"dlltest.lib");


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

{

circle c;

point p(2.0, 2.0);

c.setcentre(p);

c.setradius(1.0);

printf("area:%f girth:%f", c.getarea(), c.getgirth());


return 0;

}

  从上述源代码可以看出,由于在dll的类实现代码中定义了宏dll_file,故在dll的实现中所包含的类声明实际上为:
class _declspec(dllexport) point //导出类point

{

…

}

  和
class _declspec(dllexport) circle //导出类circle

{

…

}

  而在应用工程中没有定义dll_file,故其包含point.h和circle.h后引入的类声明为:
class _declspec(dllimport) point //导入类point

{

…

}

  和
class _declspec(dllimport) circle //导入类circle

{

…

}

  不错,正是通过dll中的
class _declspec(dllexport) class_name //导出类circle 

{

…

}

  与应用程序中的
class _declspec(dllimport) class_name //导入类

{

…

}

  匹对来完成类的导出和导入的!

  我们往往通过在类的声明头文件中用一个宏来决定使其编译为class _declspec(dllexport) class_name还是class _declspec(dllimport) class_name版本,这样就不再需要两个头文件。本程序中使用的是:
#ifdef dll_file

class _declspec(dllexport) class_name //导出类

#else

class _declspec(dllimport) class_name //导入类

#endif

  实际上,在mfc dll的讲解中,您将看到比这更简便的方法,而此处仅仅是为了说明_declspec(dllexport)与_declspec(dllimport)匹对的问题。

  由此可见,应用工程中几乎可以看到dll中的一切,包括函数、变量以及类,这就是dll所要提供的强大能力。只要dll释放这些接口,应用程序使用它就将如同使用本工程中的程序一样!

  本章虽以vc++为平台讲解非mfc dll,但是这些普遍的概念在其它语言及开发环境中也是相同的,其思维方式可以直接过渡。

  接下来,我们将要研究mfc规则dll

 

上下文章:

 

上一篇文章: VC++动态链接库(DLL)编程深入浅出(三) 下一篇文章: VC++中进程与多进程管理的方法

相关文章:

  • 在ORACLE的存储过程中如何做简单的动态查询
  • Oracle专家高级编程学习笔记[1]
  • Oracle专家高级编程学习笔记[2]
  • oracle笔记-动态SQL(1)
  • 系统DLL技术助力木马静态变动态

相关软件:

  • 《靓妹》全动态Web报表试用版Asp.NET适配件(控件) 3.0
  • 软件编程伴侣 4.0
  • 动态壁纸王 V3.4
  • 大厅动态显示系统 V1.0
  • 易语言汉语编程环境 V4.10
  • 超想中文汇编编程学习系统(中英对照版) V3.0

 

快速导航

  • 网络学院
  • 精品汇聚
  • 字体下载
  • 教程下载
  • ASP源码
  • PHP源码
  • Net源码
  • JSP 源码

认证考试分类导航

  • 微软认证
  • 计算机等级考试
  • 软件水平考试
  • 思科认证
  • Oracle认证
  • Linux认证
  • JAVA认证
  • 网络工程师
  • 系统工程师
  • 程序员

本类经典文章推荐

  • 软考系统分析师考试须知
  • C++箴言:绝不在构造或析构期调用...
  • 数据结构:判别是否为二叉排序树的...
  • 一个程序员的成长的六个阶段
  • 程序员英语试题常见硬件名和设备名
  • 从一个程序出发详细研究DataReader
  • 如何判断程序处于运行环境还是调试...
  • 程序高手必读:写好C程序的10条秘...
  • 程序员介绍:程序员的“素质”是什...
  • C++箴言:让=返回一个*this的引用

程序员阅读排行

  • 数据结构:判别是否为二叉排序树的...
  • VC++动态链接库(DLL)编程深入浅出...
  • VC++动态链接库(DLL)编程深入浅出...
  • 程序员考试:证书样本
  • 补码加、减运算规则及溢出判断
  • 数据结构:栈和递归求两顶点所有简...
  • DirectX8.0简介(8)
  • 水滴石穿·C语言之代码检查工...
  • 数据结构:打印线索二叉树的中序遍...
  • DirectX8.0简介(3)

认证考试阅读总排行

  • 全国计算机等级考试一级模拟试题01
  • 全国计算机等级考试一级模拟试题10
  • 全国计算机等级考试一级模拟试题08
  • MCSD简介
  • 全国计算机等级考试一级考试最新模...
  • 全国计算机等级考试一级模拟试题07
  • 全国计算机等级考试一级模拟试题02
  • 全国计算机等级考试一级模拟试题06
  • 全国计算机等级考试一级模拟试题03
  • 一级(WINDOWS)试题解析-Word篇

广告位置

字母检索 A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 回到顶部

关于我们 | 版权声明 | 免责条款 | 广告联系 | 软件发布 | 下载帮助 | 下载排行 | 网站地图 | 特别鸣谢 | 友情连接

copyright; 2005-2008 D9soft.com 第九软件网 版权所有