【M24】了解虚方法、多继承、虚基类、RTTI的成本
1、编译器必须实现出C++语言的特性。一般情况下,我们只需要使用这些特性就好了,不需要关心内部的实现细节。但是,有些特性的实现,会对对象的大小和成员方法的执行速度造成影响。因此,有必要了解内部实现的细节。
2、首先考虑虚方法,虚方法是用来实现多态的。多态是指对于指针和引用,表面类型和真实类型不一致的情况下,调用真实类型的虚方法。
3、虚方法有关的实现细节为:
a、父类有一个虚方法表(vtbl),可以认为是一个方法指针的数组(这里注意:对于数组,我们知道元素的类型必须一致,虚方法表中的虚方法类型是不一样的,这里进行了特殊处理),方法指针指向父类的虚方法。
b、子类整体拷贝父类的虚方法表,对于重写的虚方法,在相同位置置换为重写后的虚方法地址,对于新增的虚方法,在数组的尾部添加。
c、对于多态类的对象,内部有一个字段为vptr,指向该类的vtbl。考虑,构造子类对象,首先调用父类构造方法,将vptr初始化为指向父类的虚方法表,然后调用子类的构造方法,将vptr重置为指向子类的虚方法表。
4、需要注意的情况:
a、虚方法表是对应于类的,一个类有一个虚方法表。一般情况下,内存的消耗可以忽略。但是考虑极端的情况,一个父类有1000个虚方法,子类重写一个虚方法,并且有大量类似的子类,出现相同的方法指针,存储多次,就会导致占用大量的内存。
b、对象多一个vptr字段,如果对象本身比较小,vptr占用的内存比例就大了。
c、C++重写为什么要使用virtual关键字?从封装角度而言,类本身是个命名空间,有一个范围的概念。父类是大范围,子类是小范围,在C++中,小范围的名称会隐藏大范围的名称,而不关心名称的类型。使用virtual,其实是告诉编译器不要进行隐藏。把该方法保存到虚方法表中(可以认为是一种特殊情况的隐藏)。
d、在编译的时候,编译器只知道指针或者引用的表面类型。不同类型的指针,本质上没有区别,就是一个地址。重要的是,可以告诉编译器按照什么样的方式去解释指向的内存。这就引出一个问题,把子类对象当成父类对象来解释,不会出现问题。如何保证呢?
第一点,子类对象和父类对象在相同位置都有一个vptr,一般在头部。
第二点,子类虚方法表和父类虚方法表在位置上是一一对应的。
比如:pc->f1(); 产生的代码是:(* pc->vptr[i]) (pc); 找到第i个虚方法指针,解引用,pc传递为this指针。
e、一般情况下,重写要求:子类方法与父类方法,形参表和返回类型完全一致。但是有两种特殊情况:
重写的析构方法,子类父类的方法名各自为本身类名;
父类返回Base*,子类可以返回Derived*,目前C++支持部分的逆变协变,还不支持完全的逆变协变。
f、虚方法不能inlined,这个很好理解。inline可认为编译时文本替换,虚方法运行时确定方法的调用,二者矛盾。
5、多重继承,使问题更复杂。每个对象含有多个vptr,针对不同的父类vtbl,子类产生一个特殊的vtbl。
6、考虑,D->B->A,D->C->A,会导致A的字段在D中有两份,这显然不合理。为了解决这个问题,使用虚拟继承。B,C虚继承A。
7、考虑RTTI,C++提供关键字typeid 获取类的type_info对象。一个类对应于一个type_info对象,类及其所有的对象共享。如 int a, Person p;
typeid(a) 转化为 typeid(int) 求值;
typeid(p) 转化为 typeid(Person) 求值;
8、如果不是多态类,也就是没有虚方法,typeid(*base) 返回表面类型。如果是多态类,typeid(*base) 可以返回真实类型。这意味着内部有一定的实现方法。可以认为,在类的虚方法表中第一项就是当前类的type_info属性。这也解释了,为什么只有多态类才能用typeid求出真实类型。非多态类没有虚方法表。