关于通过不含虚析构函数的基类类型的指针删除派生类对象的问题

来源:互联网 发布:淘宝上新 新品上架 编辑:程序博客网 时间:2024/04/30 00:46
如题。问这问题时先基于一个前提条件:析构函数不含释放其他资源的代码,甚至可以是空函数,甚至甚至都可以干脆的不写。这种情况下是否仍有任何问题。
  这个问题的结论是 会导致未定义的行为(但不是内存泄漏那么简单)。具体如何就看编译器的实现了。
  我们常用的编译器,如vc、gcc等都是用的尾部追加成员的方式实现的继承(前置基类的实现方式)。这样的话在最好的情况下,可以做到对于同一个对象,整个类 和 其中的基类部分 共享一个内存起始地址(比如单继承且类和其所有基类均无任何虚函数。而这个条件实际上经常无法满足)。也就是说取对象地址,然后转换为void *或者size_t类型再输出,同用基类指针指向这个对象,然后转换指针为void *或者size_t类型再输出,将会发现这两个地址在数值上是相等的。此时如果用delete通过基类指针删除这个对象,可以认为是直接的调用了staitc void operator delete (void *);这个操作符(因为析构函数没做删除其他资源的操作)。所以不会有任何问题。当条件不满足的时候,面临的情况则和下面一种类似。
  另一些编译器(比如适用于某些嵌入式设备的编译器)却使用了先行添加本类成员的方式实现继承(或者说是尾部追加基类视图的方式)。这样即使是单继承也存在类指针数值上的改变(C++标准规定对对象取地址将始终为对应类型的首地址,这样的话如果试图取基类类型的地址,将取到的则是基类部分的首地址。而因为基类被追加到对象末端,所以就会通过在数值上增加地址来跳过派生部分)。此时如果对基类指针做delete操作,会导致很严重的后果。因为编译器从基类指针并不知道派生类是什么,所以删除操作仅能试图删除自己和自己拥有的所有基类部分。但是这个delete所使用的staitc void operator delete (void *);所传入的void*指针并不是原先new所生成的地址。这样将会导致堆内存损坏(不光是内存泄漏了)。
  而如果基类中已经提供了虚析构函数(哪怕只是个空函数)就不会导致错误了,因为通过基类指针调用delete删除派生类对象的时候,delete将通过虚函数定位机制(我这里不说虚表,因为不同的编译器实现不同,有的可能根本没有虚表这种方式)找到整个对象的首地址而不仅仅是基类部分的首地址。注意即使是这种情况下,前面说的问题仍然存在,即通过一个基类指针仍然不可能知道派生类的存在,从而不可能通过形式上的类型推导直接修正指针到最终派生对象的地址。所以这种推导一定发生在运行时(运行时无法推导类型(注意这里不关RTTI的事),而是通过推导逻辑实现)。说的简单些,当一个类的析构函数为虚函数时,通过这种类型的指针删除任一个此类的派生类对象的时候,逻辑上将等同于直接通过最末端派生类的指针删除这个对象(实现的时候多了一个指针重定位动作。但 运行时 开销极小)

  即使在继承树中的各类视图基址不共享的情况下,一般的类型转换(只要你不是将void *指针强制转换成类指针)却并不成问题(但是比前置型多出一定的运行时开销,包括按偏移量移动指针和对NULL地址的特殊处理)。对于p到pBase的隐式指针转换,编译器完全可以偷偷的将地址直接换掉(因为编译器完整的知道源类型和目标类型)。对于将pBase强制转换为p,编译器则通过开发者提供的目标类型获取转换所需的步骤。这种转换和delete删除操作是不一样的,因为仅一句delete pBase中并不包含任何p指向的对象类型的信息。

  始终需要强调的仍然是:不要写出依赖于编译器实现的代码。绝不能依赖于未定义的行为

  最后再说说题外话,为什么有编译器要设计成基类后置型的?因为一些小系统对指针的位宽比较敏感(比如可以参考8086汇编,里面的跳转,不同位宽的偏移量寻址指令速度差异巨大)。让基类视图后置可以做到本类数据成员更靠前从而地址相对于类型基址的偏移量较小,从而加快访问速度。还有些机器的偏移量寻址寄存器的位宽设计本身就比较小,不支持直接跨越较大的地址范围。

0 0
原创粉丝点击