您的位置:首页 > 其它

多继承,RTTI和虚函数

2006-02-27 16:03 134 查看
- _ -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 已经是正确的了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: