C++寫DLL导出类时,避免DLL地狱问题的完美解决方案 (转)

备注:在设计导出类时,在网上找到了这篇文章,非常的好,收藏了!网址:http://club.topsage.com/thread-497586-1-1.html

    DLL动态链接库是程序复用的重要方式,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不要导出除了函数以外的任何内容。听起来是不是有点可笑呢!
   事实上,建议你在发布导出类的DLL的时候,重新定义一个类的声明,这个声明可以不管原来的类里的成员变量之类的,只把接口函数列在类的声明里,如下面的例子:
class ClassInterface
{
privated:
    ClassInterface();
public:
    static ClassInterface * NewInstance();
    int GetXXX();
    void SetXXX();
    void Function();
};


    使用该DLL的应用程序用上面的定义作为ClassInterface的头文件,便不会有任何可能导致的安全问题。
    DLL地狱问是归根结底是因为DLL当初是作为函数级共享库设计的,并不能真正提供一个类所必需的信息。类层上的程序复用只有Java和C#生成的类文件才能做到。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了小程序应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 适合毕业设计、课程设计作业。这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。 所有源码均经过严格测试,可以直接运行,可以放心下载使用。有任何使用问题欢迎随时与博主沟通,第一时间进行解答!

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值