先看一段代码:
d类继承b类和c类,但是b类和c类又单独继承一个a类导致d中存在了两份基类a ,导致编译器无法找到正确的基类。
为了解决这种情况,设计者就设计了虚继承来解决此类情况。
这样声明的话,我们d类就可以拥有一份基类了。
如果类中声明的方法是用virtual修饰的,那么这个方法(函数)就是虚函数,而虚函数的在内存中的存储方法与普通函数是截然不同的。
当虚函数声明时,编译器会创建一个虚函数表(本质是一个函数指针数组),将当前声明的虚函数依次放入虚函数表中,然后将当前的虚函数表地址放入对象模型的最起始位置。
当我们调试时,可以看到会多出一个_vfptr,这个就是虚表指针
当我们在基类中在添加一个虚函数 e()时可以看到,_vfptr指针数组中会多一个指针成员。
#include<iostream>
using namespace std;
class A {
public:
int a;
};
class B :virtual public A {
public:
int b;
};
class C :virtual public A {
public:
int c;
};
class D : public B ,public C{
int d;
};
int main() {
D* d = new D();
return 0;
}
在这段代码中,虽然B类和C类都虚继承了A类,但是在D类中继承的B类和C类的内存空间中没有存储A类的对象,而是放在了D类的最后,并且除了D类新增加的变量或者函数外,继承的只有一个指针,这个指针就被称为虚基类指针
实现了动态绑定,即在运行时确定对象的实际类型。这样就可以实现基类指针指向派生类对象,并且能够正确调用派生类中重载的虚函数。
虚函数这么重要,因此我们可以通过它的作用知道,如果虚指针还未初始化,或者是虚表没有创建,那么调用虚函数是十分危险的(因为找不到!);那么虚函数是在哪里初始化呢?
答案是在构造函数中进行虚表的创建和虚表指针的初始化。
虚函数表是一个数组:虚函数表是一个由函数指针组成的数组,每个函数指针指向一个虚函数的地址。
顺序与声明顺序一致:虚函数表中的函数指针的顺序与类中声明虚函数的顺序一致。这样可以确保通过偏移量来访问正确的虚函数。
一个类对应一个虚函数表:每个类都有自己独立的虚函数表。如果一个类继承自另一个类,那么它会在父类的虚函数表的基础上扩展,新增的虚函数会添加到子类的虚函数表中。
类的对象共享虚函数表:同一类的所有对象共享同一个虚函数表,这样可以减少内存占用。
虚指针指向虚函数表:每个对象中都有一个隐藏的虚指针(vptr),它指向对象所属类的虚函数表的起始地址。通过虚指针,可以在运行时确定要调用的虚函数。