当一个类在实现的时候,如果存在一个或以上的虚函数时,那么这个类便会包含一张虚函数表。而当一个子类继承并重载了基类的虚函数时,它也会有自己的一张虚函数表。当我们在设计类的时候,如果把某个函数设置成虚函数时,也就表明我们希望子类在继承的时候能够有自己的实现方式;如果我们明确这个类不会被继承,那么就不应该有虚函数的出现。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data1;
};
对于虚函数的调用是通过查虚函数表来进行的,每个虚函数在虚函数表中都存放着自己的一个地址,而如何在虚函数表中进行查找,则是通过虚指针来调用,在内存结构中它一般都会放在类最开始的地方,而对于普通函数则不需要通过查表操作。这张虚函数表是什么时候被创建的呢?它是在编译的时候产生,否则这个类的结构信息中也不会插入虚指针的地址信息。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data1;
};
class B : public A {
public:
virtual void vfunc1();
void func2();
private:
int m_data3;
};
class C : public B {
public:
virtual void vfunc1();
void func2();
private:
int m_data1, m_data4;
};
- 对于非虚函数,三个类中虽然都有一个叫 func2 的函数,但他们彼此互不关联,因此都是各自独立的,不存在重载一说,在调用的时候也不需要进行查表的操作,直接调用即可。
- 由于子类B和子类C都是继承于基类A,因此他们都会存在一个虚指针用于指向虚函数表。注意,假如子类B和子类C中不存在虚函数,那么这时他们将共用基类A的一张虚函数表,在B和C中用虚指针指向该虚函数表即可。但是,上面的代码设计时子类B和子类C中都有一个虚函数 vfunc1,因此他们就需要各自产生一张虚函数表,并用各自的虚指针指向该表。由于子类B和子类C都对 vfunc1 作了重载,因此他们有三种不同的实现方式,函数地址也不尽相同,在使用的时候需要从各自类的虚函数表中去查找对应的 vfunc1 地址。
- 对于虚函数 vfunc2,两个子类都没有进行重载操作,所以基类A、子类B和子类C将共用一个 vfunc2,该虚函数的地址会分别保存在三个类的虚函数表中,但他们的地址是相同的。
- 从上图可以发现,在类对象的头部存放着一个虚指针,该虚指针指向了各自类所维护的虚函数表,再通过查找虚函数表中的地址来找到对应的虚函数。
- 对于类中的数据而言,子类中都会包含父类的信息。如上例中的子类C,它自己拥有一个变量 m_data1,似乎是和基类中的 m_data1 重名了,但其实他们并不存在联系,从存放的位置便可知晓。
int main()
{
B b;
b.vfunc1();
A a = (A)b;
a.vfunc1();
}
假如在程序中分别创建两个对象 a 和 b,a的创建是通过将b强制转化为类A得来的。对于 b.vfunc1() 的调用,应该没有太的疑问,它所调用的就是类B中的 vfunc1。而对于 a.vfunc1() 的调用,它虽然是强制转化后的结果,但并不能改变它是一个类A对象的事实,因此这里调用的便是类A中的 vfunc1,也就是上图中显示绿色的函数。
--------------------------------
int main()
{
A* pa = new B;
pa->vfunc1();
B b;
pa = &b;
pa->vfunc1();
}
将程序改写成以上内容,pa 是一个类A的指针,但它指向的是一个类B的对象。在使用pa调用 vfunc1 的时候,程序发现pa是一个指针,并且现在正在调用一个虚函数叫做 vfunc1,这时通过 pa->vptr 这个虚指针到类B的虚函数中(上图的B vtbl)找对应的虚函数地址,找到该地址以后,就用相应的虚函数来进行调用,也就是调用上图所示的 B::vfunc1()。 pa是类A的指针,为什么查找的是类B的虚函数表?
只要某一个类X包含虚函数,无论是它的父类或者它本身拥有,那么这个类的对象都会包含一个虚指针vptr,至于vptr要指向哪张表,取决于类X它本身是否含有虚函数。此处,类B中存在虚函数,那么它就会拥有自己的一张虚函数表。pa指向的是一个类B的对象,因此 p-vptr 指代的是类B中虚指针,所以它查找的是类B的虚函数表
每个类用了一个虚表,每个类的对象用了一个虚指针
虚表是在编译期就建立的,各个虚函数这时被组织成了一个虚拟函数的入口地址的数组,而对象的隐藏成员(虚拟函数表指针是在运行期),也就是构造函数被调用时进行初始化的。