深度探索c++对象模型之带有constructor类数组的new语义学

      这个还是有关我在前几篇文章谈过的“vec_new”,它只针对有constructor的类数组,像我们写【int *p = new int[5];】或【My_Simple_Class *pm = new My_Simple_Class[7]; //我们的My_Simple_Class没有定义声明constructor或destructor】,在编译器层面是没有使用vec_new的,因为它们只是单纯的开辟内存和回收内存,有new和delete就足够了。

      然而对于那些有constructor和destructor的类来说,比如我们前面的Point,就需要调用vec_new了,一句简单的【Point *p = new Point[5];】会被编译器扩展为:

Point *p;

p = vec_new(p, sizeof(Point), 5, Point::Point, Point::~Point);
如果在new的过程中发生exception【比如内存申请失败】,那么我们传递过去的destructor就需要被调用了,因为vec_new有责任在exception发生的时候把之前那些内存释放掉。

      在cfront2.0版本之前,如果我们要delete一个数组,必须要提供该数组的大小。如果我们先前写下【Point *p = new Point[10];】,那么delete的时候一定要写【delete [10]p;】。不然如果我们只写了一个【delete p】,那么程序只会释放掉数组首地址,而剩下的七个Point大小的内存就成了内存泄露。但在cfront2.0之后的版本就有所不同了【但“delete p;”仍然不行,我们可以这样写【delete []p;】,也可以达到一样正确的效果。而现在的编译器,都可以兼容这两种形式。

      对delete运算符来说,寻找数组的维度是需要效率的代价的:只有当中括号出现时,编译器才会寻找数组的维度。否则它就只会处理一个object对象!但是书中接下来的一段话,我是怎么都看不明白了,我相信作者和译者都出了一点小错:


      OK,看不懂就看不懂吧,我翻过去,接着看:应该如何记录数组元素的数目?有一个方法就是为vec_new所传回的每一个内存区块配置一个额外的word内存块,用这个内存块把相关的数组长度记录下来,而这个内存块通常被称为cookie(小甜饼)。这种cookie有一个显见的缺点——那就是如果把一个坏指针交给delete_vec,那么我们取出来的cookie很可能不正确,那么我们会得到一个不正确的指针加上一个不正确的维度大小,那么就会导致destructor做出灾难性的动作!所以有的编译器,比如说sun,是通过维护一个联合数组(associative array),在数组内放置指针和相关数组的大小,通常也会附带上destructor的函数地址,所以坏指针对于联合数组的不良影响,最大也就是取出错误的数组长度大小而已。

     在比较原始的编译器中,用两个函数来存取cookie:

//array_key是新数组的地址
//mustn't either be 0 or already entered elem_count is the count;it may be 0
typedef void* PV;
extern int _insert_new_array(PV array_key, int elem_count);

//第二个函数:从表格中取出并去除array_key
//如果不是elem_count,就传回-1
extern int _remove_old_array(PV array_key);

      下面是cfront中经过修润的vec_new的内容:

PV _vec_new(PV ptr_array, int elem_count, int size, PV constructor){
//如果ptr_array指针是NULL,则从堆中分配给该数组内存
//否则的话,以为这用户写的是【T array[count】或者【new (ptr_array)T[10]】
int alloc = 0;//我们要在vec_new中配置吗?
int array_sz = elem_count*size;

if( alloc = (ptr_array==0) )
  //全局运算符new...
  ptr_array = PV( new char[array_sz] );

//在exception handling之下,将抛出exception bad_alloc
if( ptr_array == 0 )
  return 0;

//将数组元素数目放到cache中
int status = _insert_new_array(ptr_array, elem_count);
if(status == -1){
  //在exception handling之下丢出exception
  //将丢出exception bad_alloc
  if( alloc )
    delete ptr_array;
  return 0;
  }

if( constructor ){
  register char* elem = (char *)ptr_array;
  register char* lim = elem + array_sz;
  //接下来的PF是一个typedef,代表一个函数指针
  register PF fp = PF(constructor);
  while( elem < lim ){
    //通过fp调用constructor作用于由当前elem指出的“this”元素上
    (*fp)( (void*)elem);
    elem += size;
  }
}
return PV(ptr_array);
}

按道理讲,接下来应该轮到delete_vec的介绍,但遗憾的是,原书之中一笔带过,只说两者差不多,但对于delete_vec的面貌却只字未提,所以在这里,我只能硬着头皮瞎编一个,错误疏漏是肯定有的,诚求知情者能够费神指教!!

//声明!!以下代码是我个人写的,疏漏难免!不要相信。。。
PV _delete_vec(PV ptr_array, int size, PV destructor){
int count =  _remove_old_array(ptr_array);//获取数组的元素个数
int array_sz = count*size;//获取数组的长度,单位为char*

char* p = (char*)ptr_array;
char* end = p+array_sz;
register PF fp = PF(destructor);//获取析构函数的指针地址

while( p < end ){
  (*pf)( (void *)p);//p就是析构函数传递过去的this指针
  p += size;
  }

p = ptr_array;
while( p <= end )//该释放类数组在堆中的内存块了
{ 
   delete p;
   p++;
}
}
      如果delete_vec的原理真的如上所述,仔细看,您会发现什么问题?我们还是用我们的Point类和Point3d类来说明【Point3d继承自Point类】,为方便理解,首先我们写个正常的代码:
...
Point *p = new Point3d;
...
delete p;

这种代码并不会出现问题,因为p虽然是Point类型指针,但其本质是用Point3d在堆上申请的,而通过虚拟机制,在析构和释放p内存时,程序会获得正确的虚表指针调用正确的析构函数,以及程序也会获得正确的对象内存大小。但如果这样写呢:

...
Point *p = new Point3d[10];//虽然这种写法是合法的,但它不是明智的做法,因为它会给后面的析构和释放带来困难!
...
delete []p;//哇哦,问题在这里出现了!
首先,p指针的本质是一个数组,而且所属类拥有自己的析构函数,所以它的delete势必会经由delete_vec完成,比如我们上面写的那个,那么问题就就很明显了——我们传递给delete_vec的参数有两个不对!一个是析构函数的函数地址,正确的应该是【Point3d】的析构函数,而我们传递过去的却是【Point】类的;另外一个是数组中每个元素的大小,正确的应该是【sizeof(Point3d)】,而我们传递过去的是【sizeof(Point)】,而二者很可能是不同的,这将导致我们回收的内存大小是错误的【比应该回收的小】。

      那么这个问题应该怎么解局呢?很抱歉,编译器无能为力,如果我们真的写出【Point *p = new Point3d[10];】这种屎一样的代码,那么自己屙的只能自己吞,比如这样:

for( int i=0; i<elem_count; i++)//elem_count为数组元素个数
{
  Point3d* ptr = &((Point3d *)p)[i];
  delete ptr;
} 
所以,为了正确起见,平时写代码时还是能小心就小心,虽然c++是个利器,但她毕竟是一把双刃剑。




  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值