目录
1.虚表指针与虚表
2.多态原理剖析
1.虚表指针与虚表
🍪类的大小计算规则
- 一个类的大小,实际就是该类中成员变量之和,需要注意内存对齐
- 空类:编译器给空类一个字节来唯一标识这个类的对象
对于下面的Base类,它的大小应该是类中成员变量之和,一个int成员,一个char成员,根据结构体内存对齐规则,sizeof(Base) = 8byte
class Base
{
public:
virtual void func1()
{
cout << "func1()" << endl;
}
private:
int _b = 1;
char _ch;
};
成员函数为虚函数
成员函数是普通函数
但是代码的运行结果为sizeof(Base) = 12byte,与上面的分析结果不一致
Base类中的成员函数使用virtual修饰,可以推断包含虚函数的类大小计算规则和包含普通函数的类大小计算规则可能存在差异
我们创建一个Base对象,进行进一步分析
从监视窗口,可以发现,实例化的Base对象成员构成,除了两个成员变量外,还有一个数组指针,而数组成员又是指针类型,所以准确来说,_vfptr是一个指针数组指针
🍪虚表指针和虚表
虚表指针:对象中的这个指针叫做虚函数表指针(v--virtual,f--function)。
虚表:一个含有虚函数的类中至少有一个虚函数表指针,因为虚函数的地址要被放到虚函数表(虚表)中
📖Note:
- 虚函数表本质是一个存虚函数指针的指针数组,一般这个数组最后面放了一个nullptr
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是虚函数的指针存在虚表中,可以通过这个指针找到虚函数
实例化对象中存的不是虚表,存的是虚表指针,通过这个指针可以找到虚表。
接下来执行三个操作
- Base增加一个虚函数func2和一个普通函数fun3。
- 增加一个派生类Derive去继承Base
- Derive中重写func1
class Base
{
public:
virtual void func1()
{
cout << "func1()" << endl;
}
virtual void func2()
{
cout << "func2()" << endl;
}
// 普通函数
void func3()
{
cout << "func3()" << endl;
}
private:
int _b = 1;
char _ch;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1()" << endl;
}
private:
int _d = 2;
};
从监视窗口可以发现
1️⃣基类的虚表指针值_vfptr != 派生类的虚表指针值_vfptr
2️⃣基类的普通函数func3不会存入虚表之中,继承之后也不会存入派生类的虚表
3️⃣派生类中对func1完成了重写,d的虚表中存的是重写的Derive::func1,所以虚函数的重写(语法层)也叫作覆盖(原理层),覆盖就是指虚表中虚函数的覆盖。
🍪派生类的虚表指针总结:
- 派生类对象中也有一个虚表指针,派生类继承的成员包括虚表指针,但需要注意基类和派生类的虚表不是同一份
- 基类中的虚函数,派生类继承之后放进了虚表,基类中的普通函数,派生类继承之后不会放进虚表
派生类自己新增的虚函数按其在派生类中的声明次序增加到派生类虚表的最后,如下图所示
派生类的虚表生成过程:
2.多态原理剖析
基于上面创建的基类Base和派生类Derive,执行以下代码,观察执行结果
🍪分析:
- func3是基类Base中的定义的普通函数
- func1是基类Base中定义的虚函数,且派生类完成了对func1的重写,构成多态
🍪普通函数的调用,只与调用函数的对象的类型有关
前两次对func3的调用都是Base*类型的指针进行调用
🍪多态调用,与函数调用者指向的整个对象有关
- 第一次对func1的调用:ptr指向的是一个Base对象,对虚函数func1的调用需要到_vfptr指向的虚表中查找func1函数的地址进行调用,最终调用的是Base类中的函数func1
- 第二次对func1的调用:ptr指向的是Derive对象中Base的切片,对虚函数func1的调用仍然需要到_vfptr指向的虚表中查找func1函数的地址进行调用,但是这一次,_vfptr指向的虚表中,func1函数的地址已经更改成了Derive类中重写的func1函数地址,所以最终调用的是Derive中重写的func1函数