首页 > 学院 > 开发设计 > 正文

C++中实现Java的存储管理机制

2019-11-17 05:07:18
字体:
来源:转载
供稿:网友


  众所周知,java语言最明显的优势在于用它设计的程序可以广泛地运行于互联网上所有安装了VM解释器的计算机上。然而,如今JAVA之所以在市场上如此流行,还得益于它的另一卖点:它提供了安全可靠和使用方便的存储治理机制。这是部分编程人员将它与其前身C++语言对比后所得出的结论。本文将针对两种语言的内存(以对象为单位)使用机制,通过从灵活性、易用性和效率三个方面的比较,来证实这样一个事实:在C++中可以实现与JAVA一样的存储治理机制。

  一、JAVA对象是C++对象和指针二者的继续

  JAVA作为C++的后继,在内存分配和对象使用上与之有很大的相似之处。请看下面的比较:

  表1

操作JAVAC++指针使用非指针使用声明ObjectClass InstanceObjectClass* InstanceObjectClass Instance创建Instance=new ObjectClass()Instance=new ObjectClass()声明时自动创建数据访问Instance.DataInstance->DataInstance.Data方法调用Instance.Method()Instance->Method()Instance.Method()复制指针复制Instance1=Instance2Instance1=Instance2
不提供内容复制由类自身定义不提供 缺省,或由类自身定义比较指针比较Instance1==Instance2Instance1==Instance2 不提供内容比较由类自身定义不提供缺省,或由类自身定义销毁不再引用时由垃圾收集器自动销毁delete Instance超出作用域时自动销毁
  注:

  ① C++的"指针使用"一列中并未列出形如*Instance的使用,因为这样做的实质不是指针使用;

  ②"指针复制"是指使得两个对象今后使用相同的一块内存区域,任何对此区域的修改同时会反映到这两个对象上;

  ③"内容复制"则指拷贝两个对象各自的存储区域,拷贝后内容相同,但各自保留自己的存储区,以后对任一者的修改不会影响另一者。

  从上表可以看出,除了对象销毁机制以外,JAVA的对象其实是从C++中的对象和指针共同继续而来的。
但是,很多极力提倡JAVA语言的人似乎没有意识到这种关系。他们批评C++指针的概念太难被初学者接受。的确,对初学者来说,接受计算机存储器和指针的概念并不是轻而易举的事。事实上,很多程序员都经历过这样一个迷惘的阶段。但这并不意味着存在一种对存储器的解释可以完全避免"指针"这一概念--在JAVA语言中也是如此。现在有很多讲解JAVA语言的教材,但真正能够从头到尾不出现"指针"或者类似概念(不包括抨击C++语言时的使用)的,又有几本呢?

  非凡地,JAVA初学者由于理解的障碍,经常提出像这样的问题:"为什么像int、float这样的变量使用前不需要先用new命令来创建而对象却要呢?为什么两个对象互相复制以后,修改其中一个会影响另一个,而像int、float这样的基本数据类型却不会呢?两个值相等的对象,用==比较的结果为什么是false,它们有什么是不等呢……"面对这样的问题,即使许多对JAVA比较熟悉的人有时也说不出个所以然来。究其原因,就是JAVA中的对象从来就没有离开C++指针的影子,非凡是在创建、复制(事实上,JAVA默认时只提供指针复制)和比较等最常用的操作上。因而使用它们就必须遵循指针的规则,否则将无法为计算机或编程者所理解。在C++中,指针和对象其实是与int、float共通的数据类型,但又各有其特性;继续到JAVA中以后,二者的特性互相糅合而融为一体,因此对其含义的问题就产生模棱两可的解释:JAVA对象有时是对象,有时是指针,但大多数时候是指针。

  对C++指针的另一种批评指出,C++答应指针指向任意内存区域,因此轻易引起系统的干扰,即使很有经验的程序员在使用时也难免产生疏忽。这种批评不无道理,因为大部分C++程序出错的原因都与指针有关。但由此而批评指针存在的价值是不对的。没有C++程序员愿意从不使用指针。指针是程序设计的一样利器,凡涉及内存的操作,没有指针不能做到的,并且它的效率比其他任何替代方法都高。这就是众多C++程序员宁愿冒着高度的出错风险也坚持使用指针的最大原因,而并不是他们无法避免使用指针。假如真正要像JAVA语言那样刻意避免指针的话,笔者在后面可以证实,只要他们愿意,在C++程序员同样可以做到,而且性能比JAVA更好。他们可以设计一类彻头彻尾的C++对象,而他们的使用方法却与JAVA对象一摸一样!这恐怕是许多JAVA崇拜者所始料不及的。

  本文后面所附的程序,为用户营造了这样一个编程环境:只涉及对象使用;避免指针祸害,但却保持像指针一样快速高效地访问内存的优点;像JAVA所倡导的那样,不须操心对象释放问题,在不再引用时由系统自动清理。必须强调的是,尽管该程序段理想地模拟出了JAVA的存储使用环境,编程者却确确实实在使用C++语言,并不会因此失去C++语言所具备的其他一切高效特性,甚至可以继续使用其他的指针。

更多文章 更多内容请看网络治理实用手册专题,或


  二、Agent类:用C++指针模拟JAVA对象

  为了更好的说明C++与JAVA的相似之处,笔者建立了Agent类。它通过把一个特定对象类型的指针作为自己的保护成员,来实现对C++指针的包装。


  任何使用Agent类模拟JAVA的程序必须通过如下表所示的方法来使用对象:

  表2

操作JAVA使用Agent后的C++声明 ObjectClass InstanceAgent<ObjectClass> Instance创建Instance=new ObjectClass() Instance=new Agent<ObjectClass>数据访问Instance.Data Instance().Data方法调用Instance.Method() Instance().Method()复制 指针复制Instance1=Instance2内容复制由类自身定义比较指针比较Instance1==Instance2内容比较由类自身定义销毁不需要,由程序内部自动治理
  上表显示了两种对象在使用上的惊人的相似性。本质上,两种对象是一样的,因为JAVA解释器本身就是使用与Agent相似的实现方法。但有些形式上的相似性事实上是无法做到的,因为它们究竟属于两种不同的语言,必须依照各自的规定。以下是两种需要注重的形式上的差别:在C++中,

  1、 象所属类的名称(如ObjectClass)必须放在Agent后,用〈 〉包括起来(否则,该类将与本文所讨论的Agent类毫无关系);

  2、 对象本身数据的访问和方法的调用必须在对象表识符后加一对括号,如Instance().Method(),因为Agent重定义了操作符Operator (),以帮助编译器将一个Agent的实例(如Instance)解释成用户所使用的具体某一个类(如ObjectClass)的实例,而Instance().Method()这一调用本身也等价于((ObjectClass&)Instance).Method()。

  另外,任一使用了Agent的程序必须在首部加入#include "Agent.h"才能实现对它的访问。

  以下为包含类Agent全部定义的C++头文件。由于该文件篇幅较小,所有Agent的方法均采用内联函数的形式定义。

#ifndef OBJECT_AGENT_CLASS
#define OBJECT_AGENT_CLASS

#define null 0

template<class ObjectType>
class Agent
{
int *Reference;
static bool bNewOperation;

PRotected:

ObjectType *Marrow;
void Finalize()
{
if (Reference)
{
(*Reference)--;
if (Marrow)
{
if (*Reference<=0 && Marrow)
{
delete Marrow;
delete Reference;
}
Marrow=null;
}
Reference=null;
}
}

public:

// constrUCtors
Agent()
{
if (bNewOperation)
{
Marrow=new ObjectType;
Reference=new int;
*Reference=1;
bNewOperation=false;
}
else
{
Marrow=null;
Reference=null;
}
}

Agent(ObjectType obj)
{
Marrow=new ObjectType;
Reference=new int;
*Reference=1;
*Marrow=obj;
}

// destructor
~Agent() { Finalize(); }

// convertions
operator ObjectType&() { return *Marrow; }

// operators
Agent<ObjectType>& operator=(Agent<ObjectType> obj)
{
Finalize();
Marrow=obj.Marrow;
Reference=obj.Reference;
(*Reference)++;
return *this;
}

Agent<ObjectType>& operator=(Agent<ObjectType>* obj)
{
Finalize();
if (obj)
{
Marrow=obj->Marrow;
Reference=obj->Reference;
}
return *this;
}

bool operator ==(Agent<ObjectType> obj) const
{
return Marrow==obj.Marrow;
}

bool operator !=(Agent<ObjectType> obj) const
{
return Marrow!=obj.Marrow;
}

ObjectType& operator ()() { return *Marrow; }

void *operator new(size_t size)
{
bNewOperation=true;
return new char[size];
}
};

template<class ObjectType> bool Agent<ObjectType>::bNewOperation=false;

#endif

  从源程序中可以看出,Agent类实际上是一个模版(template)。这样做的好处是,用户不必为包容自己不同类型的对象而定义之相对应的Agent类。


  Agent类的工作原理是这样的:

  当用户使用Agent<ObjectClass>来定义对象的类型时,他事实上定义的是一个Agent类型的对象。编译器自动为该对象产生一个类型为ObjectClass*,名为Marrow的成员,而ObjectClass* Agent::Marrow才真正代表用户将要建立的对象。整型成员Reference记录当前对此对象的Marrow的引用个数,当降为0时自动消除Marrow,即销毁用户定义的ObjectClass实例。当用户调用Instance=new Agent<ObjectClass>来创建对象时,分配Marrow,Reference置1,对象处于可使用状态。两个Agent实例互相赋值时,重定义的操作符operator =协助复制Marrow指针,更改公共的Reference成员,使之指示正确的引用计数。还有一个必须注重的静态成员:bNewOperation。它是一个布尔(真假值)类型的变量。当程序员用Agent<ObjectClass> Instance来定义对象时,其值为false,指出不需要真正建立Agent::Marrow的实例,因而Marrow将被赋值为null;当用Instance=new Agent<ObjectClass>来创建对象实例时,其值为true,提示构造函数建立相应Marrow的实例。其控制通过操作符operator new实现。而操作符operator ()则使得,当调用Instance()时,编译器自动返回ObjectClass类型的*Marrow,所以调用用户所需的对象成员可以使用Instance().Data和Instance().Method()。

更多文章 更多内容请看网络治理实用手册专题,或
  三、应用例子

  下面的JAVA与C++例子程序执行同样的操作:建立100000个xy类型的对象,保存到一个对象数组中,释放内存废区,如此重复10次,在结束时显示各自运行的时间。这个例子可以帮助读者了解两种语言的差异。

  1、JAVA程序如下:

public class xy //一个简单的数据类
{
int x, y;
}

public class TestTime {
static int OBJECTS=100000;
static int CHECKTIMES=10;
public static void main(String[] args) {
xy[] obj=new xy[OBJECTS];
long start, end;
long total=0, max=0, min=OBJECTS*CHECKTIMES, time;
System.out.print("PROGRESS: ");
for (int j=0;j<CHECKTIMES;j++) {
System.out.print(".");
start=System.currentTimeMillis();
for (int i=0;i<OBJECTS;i++) obj[i]=new xy();
if (j>0) System.gc(); //从第二次循环开始强制回收内存废区
end=System.currentTimeMillis();
time=end-start;
total+=time;
if (time<min) min=time;
if (time>max) max=time;
}
System.out.print("FINISHED!/r/n Minimum time in 1 check: "+min+" Milliseconds");
System.out.print("/r/n Maximum time in 1 check: "+max+" Milliseconds");
System.out.print("/r/n Average time in 1 check: "+total/CHECKTIMES+" Milliseconds");
System.out.print("/r/n Total time in "+CHECKTIMES+" checks: "+total+" Milliseconds");
}
}
  2、使用Agent类后的C++程序:

#include "stdio.h"
#include "time.h"
#include "Agent.h"

#define OBJECTS 100000
#define CHECKTIMES 10

class xy //一个简单的数据类
{
int x,y;
};

void main() {
Agent<xy> obj[OBJECTS]; //数组自动创建,不须使用new
clock_t start, end;
unsigned long total=0, max=0, min=-1, time;
printf("PROGRESS: ");
for (int j=0;j<CHECKTIMES;j++) {
printf(".");
start=clock();
for (int i=0;i<OBJECTS;i++)
obj[i]=new Agent<xy>; //operator new和构造函数被调用,创建Marrow
//对象被重新赋值时自动释放,不须像System.gc()这样的语句强制实施
end=clock();
time=(end-start)*1000/CLOCKS_PER_SEC;
total+=time;
if (time<min) min=time;
if (time>max) max=time;
}
printf("FINISHED!/r/n Minimum time in 1 check: %d Milliseconds", min);
printf("/r/n Maximum time in 1 check: %d Milliseconds", max);
printf("/r/n Average time in 1 check: %d Milliseconds", total/CHECKTIMES);

printf("/r/n Total time in %d checks: %d Milliseconds", CHECKTIMES, total);
}
  程序运行结果:



更多文章 更多内容请看网络治理实用手册专题,或   四、程序结果的分析与比较

  以下从灵活性、易用性和效率三方面对JAVA中的对象和使用Agent以后的C++对象的使用情况进行分析和比较。

  1、灵活性

  由于Agent类直接包含了C++的对象指针Marrow,所以任何C++的指针操作均可应用于Agent类上。从它派生出来的任何子类都可以对这一受保护的对象指针实施所需要的操作,使得用户获得了最大限度的灵活性。但就Agent本身而言,并不答应外部程序访问Marrow,故不存在很多人担心的指针误用的问题,因而是安全的。此外,这样做把对象的治理和使用分开来。就是说,譬如有对象Agent<ObjectClass> Instance,则Instance.Method ()指调用对象中属于Agent类的对所有对象类型均适用的方法(如存储治理、对象数据维护等功能,用户可以在Agent的派生类中自行添加),而Instance().Method()则调用属于ObjectClass本身用于实施具体操作的成员(对于绘图类的绘图操作、打印类的打印操作等等)。通过这种方法,有需要的程序员可以参与系统内部的对象治理机制,而一般的编程者则可以避免误用指针的烦恼。这种C++特有的灵活性和适应性是JAVA所不能具备的。

  2、易用性

  使用Agent类与JAVA的主要区别仅在于上文列举的两处形式上的不同点。对Agent不作任何继续和更改的情况下,程序员可以像正在使用JAVA那样,不须关心内存分配和清理,不须深入了解指针的概念,依照JAVA的步骤来使用对象。所以二者在此方面相当。

  3、运行效率

  这是大多数程序员最关心的。下面有两组测试数据,分别是上述例子C++程序(使用Microsoft Visual C++ 6.0编译运行)与JAVA程序(分别运行于IBM VisualAge for JAVA 3.0和Borland J Builder 3.0下)在两台不同型号的计算机上的运行结果,使用的操作系统为 Microsoft Windows 98,且保证测试过程中没有出现影响结果准确性的明显读磁盘现象。从表中的数据可以很清楚地看到,使用Agent类之后,C++的程序仍然比JAVA程序快很多。这是由于编译型语言对于解释型语言在速度上具有一贯的优势;另外, C++程序并不需要创建一个独立的线程来治理资源,因此其运行开销比JAVA更小。

  表3

计算机配置VisualAge for JAVA 3.0JBuilder 3.0使用Agent后Visual C++ 6.0最小运行时间(ms)最大运行时间(ms) 平均运行时间(ms)总共运行时间(ms)最小运行时间(ms)最大运行时间(ms)平均运行时间(ms) 总共运行时间(ms)最小运行时间(ms)最大运行时间(ms)平均运行时间(ms) 总共运行时间(ms)Duron 700
128M RAM13352008 1512 15122 220 270 225 2250110 170 143 1430Celeron 333
64M RAM 2504 49883778 37784440 550 466 4660 160 280 258 2580
  从这三方面比较可以看出,在对象使用上,JAVA的表现并不如预期的理想,而C++亦并不如JAVA广告中所指出的那么差强人意。JAVA摒弃指针使用的根本原因就在于JAVA必须实现其跨平台使用的优点。虽然,JAVA的任何缺点都不能掩盖这个优点,然而,为了实现这个其他众多语言都不可能达到的优点,它作出了巨大的牺牲:假如网络计算机上任一块内存都可以被远程程序访问的话,网络的安全性和稳定性就无法保障。放弃指针和代理内存的分配和回收等等措施的产生,很大程度上是由于把这些操作留给用户实在不适应网络编程的要求。出于类似的考虑,除了内存资源以外,JAVA解释器也为用户代管了大多数的计算机资源。因此,与其说这些是设计者们精心改良的成果,不如说是设计者们为了适应网络特性而采取的折中。而这种折中正是JAVA作为一门新兴网络语言得以生存的要害因素。

  五、结论

  JAVA虽然流行,但并不代表它任何创新的方面都是值得吹嘘的;C++成形虽然已经十年,但是它的优越性并未被软件业发展的潮流所冲垮,C++程序员不必对之失去信心:任何问题都应该全面地、辩证地看待。

更多文章 更多内容请看网络治理实用手册专题,或

发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表