`
zealotds
  • 浏览: 119437 次
  • 性别: Icon_minigender_1
  • 来自: 西安
社区版块
存档分类
最新评论

【转】DLL导出类及注意事项

阅读更多
文章出处

导出函数,使函数被多个程序复用,DLL中的函数实现可以被修改而无需重新编译和连接使用该DLL的应用程序。作为一名面向对象的程序员,希望DLL可以导出类,以便在类的层次上实现复用。所幸的是,DLL确实也可以导出类。

然而事实却没这么简单,导出类的DLL在维护和修改时有很多地方必需很小心,增加成员变量、修改导出类的基类等操作都可能导致意想不到的后果,也许用户更新了最新版本的DLL库后,应用程序就再也不能工作了。这就是著名的DLL Hell(DLL地狱)问题。

DLL地狱问题是怎么产生的呢?看下面的例子,假设DLL有一个导出类ClassD1:
class ClassD
{
       public:
int GetInt();
              private:
int m_i;
};
int ClassD::GetInt()
{
       return m_i;
}

应用程序使用现在的代码来使用这个类:
ClassD d;
printf(“%d”, d.GetInt());

程序进行正正常,没有什么问题。后来DLL需要升级,对ClassD进行了修改,增加了一个成员变量,如下:
class ClassD // 修改后
{
       public:
int GetInt();
              private:
                     int m_i2;
int m_i;
};

把新的DLL编译连接完成后,复制到应用程序目录,这个倒楣的应用程序调用GetInt方法恐怕再也无法得正确的值了。事实上它还算幸运的,如果GetInt的实现改成如下这样,那么它马上就要出错退出了。
int ClassD::GetInt() // 修改后
{
       return m_i++;
}

这样的事情,称它是个地狱(Hell)一点也不夸张。为什么会出错呢?我们要先从类实例的创建开始,看看使用一个类的工作过程。

首先,程序语句“ClassD d;”为这个类申请一块内存。这块内存保存该类的所有成员变量,以及虚函数表。内存的大小由类的声明决定,在应用程序编译时就已经确定。

然后,当调用“d.GetInt()”时,把申请的这一块内存做为this指针传给GetInt函数,GetInt函数从this指向的位置开始,加上 m_i应有的偏移量,计算m_i所在的内存位置,并从该位置取数据返回。m_i相对this的偏移量是由m_i在类中定义的位置决定的,定义在前的成员变量在内存中也更靠前。这个偏移量在DLL编译时确定。

当ClassD的定义改为修改后的状态时,有些东西变了。

第一个变的是内存的大小。因为修改后的ClassD多了一个成员变量,所以内存也变大了。然而这一点应用程序并不知道。

第二个变的是m_i的偏移地址。因为在m_i之前定义了一个m_i2,m_i的实现偏移地址实际已经靠后了。所以d.GetInt()访问的将是原来m_i后面的那个位置,而这个位置已经超出原来那块内存的后部范围了。

很显然,在更换了DLL后,应用程序还按原来的大小申请了一块内存,而它调用的方法却访问了比这块内存更大的区域,出错再在所难免。

同样的情形还会发生在以下这些种情况中:

1) 应用程序直接访问类的公有变量,而该公有变量在新DLL中定义的位置发生了变化;
2) 应用程序调用类的一个虚函数,而新的类中,该虚函数的前面又增加了一个虚函数;
3) 新类的后面增加了成员变量,并且新类的成员函数将访问、修改这些变量;
4) 修改了新类的基类,基类的大小发生了变化;

等等,总言而之,一不小心,你的程序就会掉进地狱。通过对这些引起出错的情况进行分析,会发现其实只有三点变化会引起出错,因为这三点是使用这个DLL的应用程序在编译时就需要确定的内容,它们分别是:
1) 类的大小;
2) 类成员的偏移地址;
3) 虚函数的顺序。

要想做一个可升级的DLL,必需避免以上三个问题。所以以下三点用来使DLL远离地狱。

1,不直接生成类的实例。对于类的大小,当我们定义一个类的实例,或使用new语句生成一个实例时,内存的大小是在编译时决定的。要使应用程序不依赖于类的大小,只有一个办法:应用程序不生成类的实例,使用DLL中的函数来生成。把导出类的构造函数定义为私有的(privated),在导出类中提供静态 (static)成员函数(如NewInstance())用来生成类的实例。因为NewInstance()函数在新的DLL中会被重新编译,所以总能返回大小正确的实例内存。

2,不直接访问成员变量。应用程序直接访问类的成员变量时会用到该变量的偏移地址。所以避免偏移地址依赖的办法就是不要直接访问成员变量。把所有的成员变量的访问控制都定义为保护型(protected)以上的级别,并为需要访问的成员变量定义Get或Set方法。Get或Set方法在编译新DLL时会被重新编译,所以总能访问到正确的变量位置。

3,忘了虚函数吧,就算有也不要让应用程序直接访问它。因为类的构造函数已经是私有(privated)的了,所以应用程序也不会去继承这个类,也不会实现自己的多态。如果导出类的父类中有虚函数,或设计需要(如类工场之类的框架),一定要把这些函数声明为保护的(protected)以上的级别,并为应用程序重新设计调用该虑函数的成员函数。这一点也类似于对成员变量的处理。

如果导出的类能遵循以上三点,那么以后对DLL的升级将可以认为是安全的。

如果对一个已经存在的导出类的DLL进行维护,同样也要注意:不要改动所有的成员变量,包括导出类的父类,无论定义的顺序还是数量;不要动所有的虚函数,无论顺序还是数量。

总结起来,其实是一句话:导出类的DLL不要导出除了函数以外的任何内容。听起来是不是有点可笑呢!
另外需要补充的是对于导出函数,如果想用LoadLibray+GetProcAddress的灵活加载方式,务必使用
extern "C" { ... }
声明之。否则导出函数经过C++的编译器函数名会变成经过修饰的含有很多随机符号的C++方法名,这种情况下使用GetProcAddress("原函数名")是不可能加载成功的。

事实上,建议你在发布导出类的DLL的时候,重新定义一个类的声明,这个声明可以不管原来的类里的成员变量之类的,只把接口函数列在类的声明里,如下面的例子:
class ClassInterface
{
       privated:
              ClassInterface();
       public:
              static ClassInterface * NewInstance();
              int GetXXX();
              void SetXXX();
              void Function();
};

使用该DLL的应用程序用上面的定义作为ClassInterface的头文件,便不会有任何可能导致的安全问题。

DLL地狱问是归根结底是因为DLL当初是作为函数级共享库设计的,并不能真正提供一个类所必需的信息。类层上的程序复用只有Java和C#生成的类文件才能做到。
分享到:
评论

相关推荐

    C++Builder调用vs2010演示代码

    3)Viewdll.exe为查看DLL导出文件的工具。 注意事项一: VSDll里面有一个 KpHttps.def LIBRARY "KpHttps" EXPORTS InitKpHttp Project > Properties... > Linker > Input > Module Definition File 里面加入你...

    tradingstrategyking:从 code.google.comptradingstrategyking 自动导出

    编译注意事项 源码包括NetTS,StkLib,StkNet,StkUI,XpFace五个工程, 使用VS.net 2005 打开工作区Tsking_vc80.sln ,然后点生成,生成解决方案,生成成功后在trunk\Bin会生成可执行文件StkUI.exe, 运行即可。 ★★1....

    股票交易源码

    编译注意事项 使用VC6打开工作区StkUI.dsw 然后可以按照以下顺序依次编译各个工程 GuiLib、profuisdll、XPFace、StkLib、StkNet、StkUI、Tools 其中工程StkUI是软件的主界面程序,Tools 是注册机以及一些...

    企业人力资源管理系统

    注意事项 =============== 如果你在使用中发现问题,或有宝贵建议的话, 请务必与我们联系。 我们的EMAIL地址是Lanpx@vip.sian.com 2002-2006(C) 【蓝色数码工作室】版权所有。保留所有权利。

    delphi 开发经验技巧宝典源码

    0033 调用Register过程的注意事项 24 0034 在Delphi中禁止使用result 24 0035 如何在Delphi中录制代码 24 0036 如何编译器会提示数组太大时的处理技巧 25 0037 如何对超大数字进行计算 25 0038 强类型的...

    delphi 开发经验技巧宝典源码06

    0033 调用Register过程的注意事项 24 0034 在Delphi中禁止使用result 24 0035 如何在Delphi中录制代码 24 0036 如何编译器会提示数组太大时的处理技巧 25 0037 如何对超大数字进行计算 25 0038 强类型的...

    e语言-易语言支持库开发手册2013版

    窗口单元实现注意事项 辅助函数: 01.易语言通知函数 02.分配内存 03.释放内存 04.分配文本数据指针 05.分配字节集数据指针 06.分配数组数据指针 07.取窗口单元指针 08.取回数据类型的类别 09.取数组数据...

    xscan

    注意事项: 1.未安装WinPCap驱动虽然可以正常使用X-Scan,但一些需要定制数据包的检测功能将完全失效。 2.端口扫描中的SYN方式和被动主机操作系统识别功能在NT4系统下无法使用,在windows 2000等系统下使用时...

    asp.net知识库

    .net 2.0 访问Oracle --与Sql Server的差异,注意事项,常见异常 Ado.net 与NHibernate的关系? 动态创建数据库 SQL Server数据库安全规划全攻略 .net通用数据库访问组件SQL Artisan应用简介1 在Framework1.0下...

    仿淘宝网程序无错版

    七、注意事项 配置web.config 为保证系统安全运行,以防网站被黑或数据被窃,请一定要进行如下的安全设置。注意以下设置,建议在程序上传到虚拟主机空间或服务器上前进行。 数据库连接安全 此设置必须在文件上传到...

    Access+2000中文版高级编程

    12.3 创建及完善简单的数据访问页 369 12.3.1 数据访问页的字段列表 369 12.3.2 添加超级链接 370 12.3.3 在数据访问页中使用表达式 374 12.3.4 使用绑定的组合框和列表框 375 12.3.5 使用主题格式化 378 ...

    Access 2000中文版高级编程(part1)

    Access2000中文版高级编程 目录 第一部分 编程基础 1 第1章 宏与代码 3 1.1 何时使用代码代替宏 4 1.1.1 何时用宏 4 1.1.2 何时用代码 4 1.2 宏到代码的转换 5 ...15.5 使用API调用的注意事项 479 15.5.1 ...

    网管教程 从入门到精通软件篇.txt

    DUN:Microsoft拔号网络导出文件 DV:数字视频文件(MIME) DWG:AutoCAD工程图文件;AutoCAD或Generic CADD老版本的绘图格式 DXR:Macromedia Director受保护(不可编辑)电影文件 E EDA:Ensoniq ASR磁盘映像...

    PL/SQL Developer8.04官网程序_keygen_汉化

     您可以在任何SQL或PL/SQL源文件中使用To-Do条目快速记录该文件中那些需要进行的事项。以后能够从To-Do列表中访问这些信息,访问操作可以在对象层或工程层进行。 对象浏览器  可配置的树形浏览能够显示同PL/SQL...

    PLSQLDeveloper下载

    如果您具备创建DLL的编程语言,您还可以自己编写插件。 多线程IDE——PL/SQL Developer是一个多线程IDE。这样,当SQL查询、PL/SQL程序、调试会话等正在运行时,您依然可以继续工作。而且,该多线程IDE还意味着出现...

Global site tag (gtag.js) - Google Analytics