多继承,RTTI和虚函数

  - _ -b 最近喜欢在CSDN上写长文抢分...  不能抢一,就发他个千文贴,楼主揭帖的时候一看哟这么长,100分怎么也得给个80罢?

 

    早在一个月前,讨论某个多继承相关的bug的时候,我就想写这个主题了。但是那时候对高层概念知之甚少,怕有所偏颇。 前几天恰好看到一个百分贴《RTTI一定要求有虚函数吗?》,一不小心,回复了近2K文字,俨然一篇整文...  既然已经写了,那就干脆用上把。

 


  

    问题 1  多继承、向上映射有冲突? C++ 的设计错了吗?

    初的时候,C++只有单继承。很多人要求多继承,然而多继承就必然涉及到某种程度的类型识别问题!(阿?会吗?) 看下面一个多继承的例子。看出有什么错误吗?

class A{ int i; };
class B{ int i; };
class C: public A, public B{};
 
void delB( B* pb ){
    delete pb;
}
 
int main(){
    B* pc = new C;
    delB( pc );
}

      B是C的公开基类, 亦即:C的对象是B的对象,所以C++甚至鼓励这样做: B* pc = new C; 

 

      但是,上面的程序实际上是错误的。 在VC7.1 Debug 模式下会出现一个保护性测试报错 _BLOCK_TYPE_IS_VALID( pHead->nBlockUse )。为什么呢?

      问题来自于多继承。A和B大小为4,假如C的地址为 2000,则C::B的地址为 2004 ! 所以 pb = 2004,和C的实际分配地址产生了所谓“内存错位”现象!下面的操作将试图删除2004(而不是2000)的内容 :

void delB( B* pb ){
  delete pb;
}

 

    花开两支,各表一支。这一支暂且按下不表,先看看另一支:

 


 

     问题 2  typeid一定要虚函数么?

    几天,C++区有人提下面的问题(稍有删节):

 

    基类如果没有虚函数。输出结果(rtti)就不对。不知道为什么。环境:win2000、vs2003、控制台工程。/GR

class BaseClass{
    // 如果把这行注释掉。下面的输出结果就不正确了。(应该是DerivedClass1,却变成了BaseClass)为什么。(gcc也是这种情况)
    virtual void vfunc(){}; 
};
class DerivedClass1 : public BaseClass{};
class DerivedClass2 : public BaseClass{};
 
int _tmain(int argc, _TCHAR* argv[]){
    DerivedClass1 obj1;
 
    BaseClass*p = &obj1;
    std::cout << typeid(*p).name() << std::endl;
}

 

    其实,这根本不是错误。ISO C++98 规定,涉及继承的 RTTI 都要求对象是“多态类型”:

5.2.7 Dynamic cast
条款6 在其他情况下, v 必须是多态类型(10.3)的指针或者左值。

5.2.8 Type identification
条款2 当type_info作用于多态类型(10.3)左值的时候,结果是描述最后继承对象的 type_info 对象(即,动态对象)。

    那么多态类型是什么呢?

10.3 Virtual functions
条款1 ... 一个包含或者继承虚函数的class 被称为多态 class

    看来虚函数和RTTI的确存在深刻的联系。但是,C++ 为什么要这样规定?

 

 


 

    们要知道C++不是透明的语言。 若要真正理解C++,就必须知道他的“背后”完成了什么。 C/C++很吝啬而高效的。他们的执行是如此面向底层,以至于可以与汇编媲美。

 

    一个普通的结构,C++绝对不会往里面放入多余的东西。 RTTI是OOP的强力支持,但是同样有额外开销。 游戏程序员甚至声称:“不要用RTTI和虚函数,他们会显著且莫名其妙的影响性能”—— RTTI容易造成Cache丢失。 但他们仍然大量使用C++,因为 C++ 让你能自由选择要支持还是要开销 —— java 、C# 等语言则强制捆绑了所有特性。

 

    下面解释RTTI的原理,我们用一个一般的32位C++编译器:

    假定 BaseClass 不是多态类型,他没有虚函数,如下:

class BaseClass {
    int i, j;
};

    显然它只有8个字节(两个int)空间。 他必须和C的struct开销等价,否则很多人就会抛弃C++了。

 

    下面有一个基类指针:

BaseClass* p = new DerivedClass;

    系统如何知道 p 指向什么东西?没办法知道呀。p 只拥有前8个字节的访问权,这8个字节已经有用了。也许 DerivedClass 可以在第9个字节放上什么信息, 但是 p 绝对不该访问这个字节 —— 既然 BaseClass 没有纯虚函数(即可以实例化), 万一 p 真的指向 BaseClass 类型,岂不是越界访问了?

 

    聪明的读者啊,你能想出一个不增加 class 大小,又能支持RTTI的算法吗? (难道你打算让C++提供一个虚拟机,对所有内存进行索引? 这显然会得到更BT的开销 )

 

    那么系统如何支持RTTI的呢?答案是借用虚函数表

     

 


 

     于C++有一个常识是:假如一个类用于继承,那么它的析构函数一定要是虚函数(除非你有非常特殊的理由) —— 否则对基类指针执行delete,派生类的析构函数就被忽略了。即使派生类无需作额外析构工作, 后面我们还有一个更大的理由。 看这个BaseClass, 由于增加了一个虚析构函数,他就变成了多态类

class BaseClass {
    int i, j;
public:
    virtual ~BaseClass();
};

    为了实现多态,C++ 已经有了虚函数表的概念。 当某个类包含虚函数的时候,系统就要晚绑定动态调用它。 所以,这样的类就要承担额外的时间和空间开销:他的大小会增加4字节,这个 VPTR 指针指向类对应的虚函数表。每个基类和子类都有单独的虚函数表,构造函数调用时会设置当前的 VPTR。 于是多态就很容易实现了。

     所以,上面的 BaseClass 的大小变成了12字节。VC的虚函数表在第一个DWORD上,某些Linux下的编译器虚函数表在最后一个DWORD上。

    假如有一个BaseClass* p,那么系统可以传递他的地址作为 this,从虚函数表中找到对应函数执行调用。

 

    当标准化委员会决定增加RTTI特性的时候,他们把目光放在了虚函数表上。一旦打开 RTTI 选项,虚函数表里面就增加了多个子表的引用。

BaseClass* p = new DerivedClass;

    当你要求RTTI操作时,编译器产生代码察看 p的 VPTR,索引到其中需要的子表,完成操作。这也是为什么VC 关闭 RTTI 时不能正确 cast 的原因 —— 那种情况下虚函数表中只包含虚函数。

 

 


   涉及到多继承的时候,不只是delete,虚函数调用同样出现了危机:基类和派生类拥有不同的this,却可以调用同样的虚函数 —— 如何传递正确的 this ?

    考虑一个基类虚函数f ,他被多继承的派生类 Derived 覆盖。

struct B{
    virtual void f();
};

struct Derived: public A, public B{
    virtual void f();    // 覆盖B::f
}

Derived* pd = new Derived;         // 假定返回2000
B*  pb   = pd;                                   // 指向 Derived::B,  结果可能是2004

    现在的问题是, 地址为2000的 pd 和 2004的 pb 都可以调用 f, 但无论怎么调用 f 必须获得 this 指针 2000 —— 这需要一个向下的修正。 怎么修正呢?

    VC 的处理很聪明:  B::f 是B的虚函数,所以就应该传递B* 。 于是,无论你通过基类还是派生类调用f,传递的都是 pd->B 的地址 2004, 而 Derived::f 则知道自己收到的是 Derived::B的地址,从而减去4得到正确地址2000。

 

    回到最初的问题。派生类指针不知道自己的真正this, 但是他的虚函数知道。 所以大部分编译器采取这样的策略:通过虚析构函数(而不是delete运算符)完成空间释放操作。

 

    当VC执行 delete bp 时, 如果bp的析构函数为虚函数,他并非在原地删除bp,而是 push 一个“删除标志”,然后调用 bp 的当前析构函数 —— 即最晚派生类的析构函数。

    最晚派生类肯定拥有自己的析构函数 —— 如果你不定义,那么编译器会产生一个默认的版本。 在析构函数尾部,他检查删除标志,根据情况 operator delete this。 根据上面所说的,我们知道这个 this 已经是正确的了。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值