书接上文,虚函数机制是cpp实现动态多态的一种方式。简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
出现继承的情况 --存在虚函数
当一个类存在虚函数的时候,那么编译器就会给它生成一个虚函数表。并在该类中添加一个成员变量--虚函数表指针。编译器会保证这个指针处于类的最开头,以保证性能。
举个例子,存在这么一个类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
那么在内存里,这个类的实例会长成这样:
虚函数表的末尾有一个标志节点,在出现多继承的时候可能会出现多个虚函数表,这个节点是用来表示这个表之后是否还存在其他表的。根据编译器实现的不同各不相同。
有了对虚函数基类的基本认识,现在就可以讨论继承的情况了。以下情况基于GCC,不同编译器对于父子类虚函数表的处理也不同。
一般继承 --无虚函数覆盖情况
直接看图
可以看到,B拥有自己的新虚函数表,由于B的虚函数与A没有重名的部分,即不发生覆盖(重写override)所以B的虚函数表会把父类的虚函数按顺序存放,之后跟上自己的虚函数。而成员变量依然遵照前面的规则,按顺序继承。不同的是,成员变量的第一位加了一个虚表指针。
一般继承 --虚函数覆盖原则
一样来看图:
稍微解释一下,Child类重写了Parent类的f(),并新增了两个虚函数,GrandChild类同样重写了f(),并重写了 Child类 中的g_child(),另外添加了一个自己的虚函数。
它在内存里长这样:
由于都是单一继承,所以只有一个虚函数表。在Child继承Parent的时候,虚表长这样:
重写的f()替换了父类对应的虚函数的位置,自己的虚函数跟在最后。
而GrandChild继承 Child 的时候,做出了同样的替换和追加,所以最后的虚函数表结构就成了这样。
成员变量依然按照之前的规则,按照继承顺序依次排列。
多重继承
上图:
出现多重继承的情况时,每一个基类都有自己的虚函数表,细想一下,虚函数表在类的内部是以成员变量-->虚表指针来表示的,也就是跟之前的成员变量继承规则一样,有三个虚表指针!
内存里长这样:
派生类同时重写了三个基类的共有函数:f(),那么在三个虚函数表中,对应位置的函数指针都被替换成了子类,此举可以保证无论是哪一个父类的指针,在指向派生类的时候都能正确调用函数。子类自己的虚函数,会跟在第一个虚函数表末尾。
对于成员变量来说,也是同样的道理,把虚表指针看作父类的一个成员变量,自然是Base1的虚表指针后跟上Base1的成员变量,之后才轮到Base2。
重复继承
存在如下继承情况:
B1和B2继承于同一个基类B,既然是多继承,那么D也会存在两个虚表指针,在内存里长这样:
可以看到,继承来的两个虚表都存在一个基类B的函数指针Bf(),出现了重复,但问题不大,毕竟函数也正确重写了。真正的问题出现在成员变量上。
B1继承了一个B.ib,B2也继承了一个B.ib。这时候会发现,D中出现了两个ib成员。直接调用D.id的时候,就会出现二义性。必须使用D.B1::ib和 D.B2::ib来分别指定。
为了解决重复继承带来的数据冗余和二义性,C++引入了虚拟继承的概念。
虚拟单一继承
先来看看在单一继承中,虚拟继承方式都干了啥:
假设存在如下结构:
class A {
private:
int ida;
public:
virtual fun();
virtual funA();
}
Class B : virtual public A {
private:
int idb;
}
那么对于classB来说,其内存空间长这样:
注意,虽然A中有虚函数,但这里B中并没有虚表指针,取而代之的是一个 V_Base 即虚基指针。从A继承来的东西被放在了B的最后头。其中包括A本身的虚函数表和成员变量。
虚基指针的作用就是用来代替直接继承A的所有成员,转而使用一个指针做跳转。虚基表V_BASE_TABLE里存有两个值,第一个是当前位置到当前类虚表指针的偏移量。由于这里B没有虚表指针,所以是0。第二个值代表当前类的起始位置到共享的基类对象的偏移量,这个例子中,系统为32位,虚表指针4字节,int成员4字节,之后就是共享基类对象的部分了。所以这个值是8。
如果B中出现了虚函数和虚函数覆盖的情况呢?类的结构如下:
class A {
private:
int ida;
public:
virtual fun();
virtual funA();
}
Class B : virtual public A {
private:
int idb;
public:
virtual fun() override;
virtual funB();
}
此时的内存结构:
由于B中出现了新的虚函数,所以多出了自己的虚表指针。但其中仅有新的虚函数,重写的虚函数在基类对象A的虚函数表中被修改。
再来看虚基类表,此时由于多出了一个虚表指针,所以虚基指针到虚表指针的偏移量变成了-4,而类到共享基类的偏移量变成了12。
复杂的虚拟多继承
见识一下什么叫做丧心病狂
按顺序来处理,首先是B1,B2的内存结构,这两个内存结构相似,虚继承了B,并且重写了funB(),所以内存长这样:
最后来个大杂烩
重点关注这个V_BASE_A,由于C虚继承A,所以C的成员中会添加一个新的虚基指针,对应虚基表的偏移量的计算就要注意。由于当前类的虚表指针和继承而来的B1共用(c的虚函数也被追加到表后),所以虚表指针就是最开头那个,计算偏移量就从开头开始。而到公用类的偏移量也是一样,相对C的开头,也是最开头。
而 V_BASE_B2计算偏移量的时候就是相对于继承而来的V_TABLE_B2来计算的了。
公用基类对象也是按顺序,存放于C的末尾,当然,该覆盖的函数指针一样覆盖。这里其实是为了好理解才说进行覆盖。实际编译器可能将所有的虚函数放到一个表里,被覆盖的地方使用一句跳转指令。这就是题外话了。
叨叨几句... NOTHING