上一篇博客详细陈述了类库开发的各个知识点(http://blog.csdn.net/z702143700/article/details/45989993),本文将进一步陈述,对于类库开发过程中导出类的开发规范问题。

C/C++开发的DLL当初是作为函数级共享库设计的,并不能真正提供一个类所必需的信息。类层上的程序复用只有Java和C#生成的类文件才能做到。所以,当我们在用C++开发类库时经常会遇到类的设计问题,设计不好,就会出现DLL地狱。

DLL地狱

导出类的DLL在维护和修改时如果增加成员变量、修改导出类的基类等操作都可能导致意想不到的后果,也许用户更新了最新版本的DLL库后,应用程序就再也不能工作了。即著名的DLL Hell(DLL地狱)问题。

DLL地狱问题的产生

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

举个例子:

假设DLL有一个导出类ClassTool:

class ClassTool
{
public:
int GetVar();
private:
int m_var1;
};
int ClassTool::GetVar()
{
return m_var1;
}

应用程序使用该类:

ClassTool ct;
printf(“%d”, ct.GetVar());

由于需求改变需要给类加一个变量:

class ClassTool //修改后的类
{
public:
int GetVar();
private:
int m_var2;
int m_var1;
};

把更新后的DLL编译连接完成后,复制到应用程序目录,这个倒霉的应用程序调用GetVar方法后,恐怕再也无法得正确的值了。这样的事情,称它是个地狱(Hell)。

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

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

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

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

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

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

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

导出类设计规范

在进行导出类设计时就应该遵循以下默认的规范: 
1.不直接生成类的实例。对于类的大小,当我们定义一个类的实例,或使用new语句生成一个实例时,内存的大小是在编译时决定的。要使应用程序不依赖于类的大小,只有一个办法:应用程序不生成类的实例,使用DLL中的函数来生成。把导出类的构造函数定义为私有的(privated),在导出类中提供 静态(static)成员函数(如NewInstance())用来生成类的实例。因为NewInstance()函数在新的DLL中会被重新编译,所以,总能返回大小正确的实例内存。也可以使用全局函数的方式返回类的一个实例(就像我上一篇博客中的示例中那样)。

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

3.尽量不使用虚函数。就算有也不要让应用程序直接访问它。因为类的构造函数已经是私有(privated)的了,所以应用程序也不会去继承这个类,也不会实现自己的多态。如果导出类的父类中有虚函数,或设计需要(如类工厂模式之类的框架),一定要把这些函数声明为保护的(protected)以上的级别, 并为应用程序重新设计调用该虑函数的成员函数。这一点也类似于对成员变量的处理。 
如果导出的类能遵循以上三点,那么以后对DLL的升级将可以认为是安全的。 
另外,如果对一个已经存在的导出类的DLL进行维护,同样也要注意:不要改动所有的成员变量,包括导出类的父类,无论定义的顺序还是数量;不要动所有的虚函数,无论顺序还是数量。 
导出类设计时,记住不要导出除了函数以外的任何内容。

建议,你在发布导出类的DLL的时候,重新定义一个类的声明,这个声明可以不管原来的类里的成员变量之类的,只把接口函数列在类的声明里,如下面的例子:

    class ClassInterface
{
privated:
ClassInterface();
public:
static ClassInterface * NewInstance();
int GetVar();
void SetVar();
void Function();
};

在应用程序中,使用ClassInterface作为类的头文件,这样可以避免任何可能导致的安全问题。

(转)C++类库开发之导出类设计原则的更多相关文章

  1. 适用于Java开发人员的SOLID设计原则简介

    看看这篇针对Java开发人员的SOLID设计原则简介.抽丝剥茧,细说架构那些事——[优锐课] 当你刚接触软件工程时,这些原理和设计模式不容易理解或习惯.我们都遇到了问题,很难理解SOLID + DP的 ...

  2. JavaScript设计模式之设计原则

    何为设计 即按照哪一种思路或者标准来实现功能,功能相同,可以有不同的设计方案来实现 伴随着需求的增加,设计的作用就会体现出来,一般的APP每天都在变化,更新很快,需求不断在增加,如果设计的不好,后面很 ...

  3. Java 程序员应在2019年学习的10条面向对象(OOP)设计原则

    面向对象的设计原则 是 OOP 编程的核心,但是我看到大多数 Java 程序员都在追求诸如 Singleton 模式,Decorator 模式或 O​​bserver 模式之类的设计模式,而对学习面向 ...

  4. 并发编程概述 委托(delegate) 事件(event) .net core 2.0 event bus 一个简单的基于内存事件总线实现 .net core 基于NPOI 的excel导出类,支持自定义导出哪些字段 基于Ace Admin 的菜单栏实现 第五节:SignalR大杂烩(与MVC融合、全局的几个配置、跨域的应用、C/S程序充当Client和Server)

    并发编程概述   前言 说实话,在我软件开发的头两年几乎不考虑并发编程,请求与响应把业务逻辑尽快完成一个星期的任务能两天完成绝不拖三天(剩下时间各种浪),根本不会考虑性能问题(能接受范围内).但随着工 ...

  5. atitit.提升研发效率的利器---重型框架与类库的差别与设计原则

    atitit.提升研发效率的利器---重型框架与类库的差别与设计原则 1. 框架的意义---设计的复用 1 1.1. 重型框架就是it界的重武器. 1 2. 框架 VS. 库 可视化图形化 1 2.1 ...

  6. atitit.提升研发效率的利器---重型框架与类库的区别与设计原则

    atitit.提升研发效率的利器---重型框架与类库的区别与设计原则 1. 框架的意义---设计的复用 1 1.1. 重型框架就是it界的重武器. 1 2. 框架 VS. 库 可视化图形化 1 2.1 ...

  7. C++类库开发详解(转)

    前言:这是一篇总结性的文章,需要有一点C++和dll基本知识的基础,在网上查阅了很多资料感觉没有一篇详细.具体.全面的dll开发介绍,我这是根据最近项目和网上资料整理出来的,并附带实例的一个总结性的文 ...

  8. DLL 导出类

    MyMathFun.h #pragma once // #ifdef DLLCLASS_API // #define DLLCLASS_API _declspec(dllimport) // #els ...

  9. Java 类设计技巧

    摘自<Java核心技术>卷I:基础知识 p140 第4章对象与类 - 类设计技巧 1)一定将数据设计为私有. 最重要的是:绝对不要破坏封装性.有时候,需要编写一个访问器方法或更改器方法,但 ...

随机推荐

  1. PIE SDK打开矢量数据

    1. 功能简介 GIS将地理空间数据表示为矢量数据和栅格数据.矢量数据模型使用点.线和多边形来表示具有清晰空间位置和边界的空间要素,如控制点.河流和宗地等,每个要素被赋予一个ID,以便与其属性相关联. ...

  2. 从零开始学Linux(11)--more

    more命令,是将文件的内容从上到下显示在屏幕上. more会以一页一页的显示方便使用者逐页阅读,而最基本的指令就是按空格键(space)就往下一页显示,按 b 键就会往回(back)一页显示,而且还 ...

  3. lnmp 一键搭建脚本

    转载注明出处!!!!!!!!! 不足之处望多多指教. 不明之处站内私. #!/bin/bash #################################################### ...

  4. Android: 通过Runtime.getRuntime().exec调用底层Linux下的程序或脚本

    Android Runtime使得直接调用底层Linux下的可执行程序或脚本成为可能 比如Linux下写个测试工具,直接编译后apk中通过Runtime来调用 或者写个脚本,apk中直接调用,省去中间 ...

  5. spark运行时出现Neither spark.yarn.jars nor spark.yarn.archive is set错误的解决办法(图文详解)

    不多说,直接上干货! 福利 => 每天都推送 欢迎大家,关注微信扫码并加入我的4个微信公众号:   大数据躺过的坑      Java从入门到架构师      人工智能躺过的坑          ...

  6. Js简易代码生成工具

    代码 javascript:(function(){ document.body.innerHTML = '<textarea id="txtTemplate" style= ...

  7. Python 进阶

    高阶函数 定义 函数接受的参数是一个函数 函数的返回值为一个函数 满足以上2点中其中一个就是高阶函数 函数嵌套 定义 函数中def定义一个函数 嵌套会存在闭包, 其他情况不会有闭包(闭包闭的是变量) ...

  8. Jvav Collection-List

    package 集合; import java.util.ArrayList; import java.util.Collection; /** * 集合和数组的区别: * 1.长度 * 数组长度固定 ...

  9. 纯代码编写的vc跳转SB

    今天遇到个问题,我整个项目都是纯代码,突然有个引用的VC用了storyboard,导航的跳转不知道如何操作,最后试了很多方法总算可以了 首先,找到要跳转的sb. UIStoryboard *story ...

  10. JavaScirpt(JS)——DOM文档对象模型

    一.HTML DOM介绍 HTML DOM 是 W3C 标准(是 HTML 文档对象模型的英文缩写,Document Object Model for HTML). HTML DOM 定义了用于 HT ...