多态
虚函数表
派生类虚表的生成
多态类的对象模型
多态原理总结
先声明一个概念 : 虚函数表就是虚表
虚函数表
我们先来看一段代码, 探究一下虚函数表究竟是什么东西。
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1 ;
char _c;
};
int main ()
{
Base b;
cout << sizeof(b) << endl; //12
system("pause");
}
我们思考一下Base对象是多大呢 ? 也就是sizeof(b)是多少呢?
答案是12。 那为什么是12个字节 ? 里面只有一个int类型和一个char类型的变量,内存对齐之后,也是8个字节, 为什么是12个字节呢?
我们可以通过监视窗口看一下有什么异同的地方?
我们通过监视窗口,可以看到,除了_b和_c成员之外,还多了一个_vfptr放在对象的前面,对象中的这个指针我们叫做虚函数表指针 (也叫做虚表指针)。
而且一个含有虚函数的类中都至少有一个虚函数表指针(_vfptr),因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
让我们通过派生类再来看一下倪端。
class Base
{
public:
void Func()
{
cout << "Func()" << endl;
}
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
private:
char _c;
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Derive d;
cout << sizeof(d) << endl;
system("pause");
}
那么sizeof(d)是多大呢?? 派生类的对象d是多大呢?? 是否含有两个虚表?
答案是:16
派生类没有属于它自己的虚函数表指针。
我们再来看一下监视窗口
b是基类Base的对象,d是派生类Derive对象
我们通过监视窗口可以看出来。每一个对象(在这里是b对象和d对象)都有一个虚表。
派生类d对象中的虚表中func1已经是自己的了,已经是重写的这个虚函数了。func2还是b对象中的,没有动。
所以虚函数的重写还有另外一层意思,虚函数的覆盖。重写是语法层的,虚函数的覆盖是实现层次的。
所以 覆盖是在子类的虚函数基础上, 虽然和子类不是同一块虚表,但是把子类的虚表拷贝一份,对重写的虚函数地址进行覆盖,覆盖自己重写的那个虚函数,再把子类定义的虚函数地址加在虚函数表的后面。
派生类虚表的生成:
a: 先将基类中的虚表内容拷贝一份到派生类表中
b: 如果派生类中重写了基类中的某个虚函数,用派生类自己的虚函数覆盖虚函数表中基类的虚函数
c:派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
通过观察我们可以知道
- 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针
- 基类b对象和派生类d对象的虚表是不一样的,
多态类的对象模型(图中以Base类为例)
我们通过多态类的对象模型再来捋一下关系。(还是Base类)
每一个含有虚函数的类的对象中都会有含有一个虚表指针_vfptr,指向虚表。 虚表是一个函数指针的数组,虚表中存放的是类中每一个虚函数的地址,(比如图中的Func1和Func2的地址都存在虚表中)。 虚表指针其实就是这个虚表(函数指针数组)的首地址。
就如监视窗口中的Base类对象b中的虚表中存放着Func1函数的地址(0x00aadc84)和Func2函数的地址(0x00aa14ba)。(地址不一定连续)
那么虚函数存、虚表、虚函数的地址存在哪?
- 虚函数是存在代码段中
原因:函数被编译完成之后, 都是指令,第一句指令就是函数的地址。 调用函数的时候,就是call 这个地址,都是存在代码段中。
所以和普通函数一样,都是存在代码段中
虚函数表指针, 全称叫做虚函数表指针,虚函数表其实是一个指针数组。
那么多态的原理是什么?
我们这一节了解到虚表和虚表指针后,那么要想使父类指针指向父类调用父类,指向子类对象调用子类,是怎么样做到的呢?
基类指针ptr指向基类的b对象,去b对象的第一个位置就可以找到虚函数表的地址,找到虚函数表中就可以找到要调用的Func1函数的地址。
基类指针ptr指向派生类的d对象时,[ 看到的还是基类的一部分,因为是基类的指针,看到的都是属于基类的那一部分(完成"切片") ](不理解没有太大关系,可以忽略),都是到虚函数表中去找这个函数的地址,但是指向子类的时候,子类重写的虚函数已经将父类的虚函数给覆盖了,所以调用的就是子类重写后的虚函数。
所以就达成指向谁调用谁。 多态实现的真正原理依靠的是虚表。
明白了原理 ,再来思考一下,多态的形成条件为什么要用指针和引用
因为多态的实现中,基类的指针指向派生类,可以看见派生类中的虚函数表,并且派生类的虚函数表中已经对基类的虚函数进行了重写,所以我们可以直接通过指针或引用找到派生类虚函数表的虚函数。并且调用。
为什么派生类向基类赋值不行呢?因为赋值的话,只能将成员变量的值赋值过去,虚函数表不能进行赋值,所以形成多态的时候我们要指针或者引用进行调用。