文章目录
1. 什么是多态
通过一段示例代码来说明。
1.1 定义基类和子类
class Base {
public:
virtual void PrintTestA()
{
printf("Base Test A\n");
}
virtual void PrintTestB()
{
printf("Base Test B\n");
}
protected:
int32_t protectVar = 1;
private:
int32_t priVar = 2;
};
class Child : public Base {
public:
virtual void PrintTestB()
{
printf("Child Test B\n");
}
private:
int32_t childVar = 3;
};
基类Base有2个虚函数,以及2个成员变量。 子类Child重写了PrintTestB, 并且有一个成员变量。
1.2 基本的多态场景
Child child;
Base* pBase = &child;
pBase->PrintTestA();
pBase->PrintTestB();
父类指针pBase指向的实际对象类型为子类child 当调用PrintTestA时,由于子类未重写PrintTestA,所以调用父类的PrintTestA 当调用PrintTestB时,由于子类重写了PrintTestB,所以调用子类的PrintTestB
这就是常说的多态。下面深入分析一下,C++底层是如何实现多态的。
2. 多态的实现原理
2.1 从对象的内存布局看多态
继续上面的代码,我们打印一些内存信息。
printf("\npBase addr: 0x%x\n", pBase);
int32_t* mem = (int32_t*)pBase;
printf("memory: 0x%x 0x%x 0x%x 0x%x\n",
mem[0], mem[1], mem[2], mem[3]);
int32_t* pVt = (int32_t*)mem[0];
printf("vtable [0]: 0x%x [1]: 0x%x\n\n",
pVt[0], pVt[1]);
pBase的值为0x6ff9fc,即child对象存放的首地址为:0x6ff9fc 将pBase强转为int32_t* mem数组指针,打印了连续4个int32_t的值:0xc47b64 0x1 0x2 0x3,其实就是child对象的内存值 mem[0]是虚表指针,保存的虚表首地址:0xc47b64 mem[1] mem[2]是父类Base的2个成员变量protectVar和priVar(对象的内存布局是先放基类成员,再放子类成员) mem[3]是子类Child的成员变量childVar 最后,又将mem[0]强转为int32_t* pVt,方便打印虚表的内容。 由于一共2个虚函数,子类只是重写了其中1个,所以虚表一共有2项:0xc41091 0xc41190,他们是Base::PrintTestA和Child::PrintTestB的实际函数地址。 用一个图来说明概括:
从内存可以看出,C++是通过给存在虚函数的对象开头存放一个虚表指针,指向实际类型的虚表,从而能够调用正确的函数。Base、Child有各自的虚表。
2.2 从反汇编看多态
2.2.1 对象的虚表指针什么时候初始化
在Child构造函数中,先调用父类构造函数,然后将Child类型的虚表地址赋值给虚表指针。
2.2.2 如何通过虚表指针调用到函数
pBase->PrintTestA 前面先通过pBase拿到开头4个字节的虚表指针,放在edx寄存器,红框部分,通过edx取虚表第1项,即: Base::PrintTestA
pBase->PrintTestB 与上面类似,注意这里的edx+4,相当于偏移4个字节,取虚表的第2项,即:Child::PrintTestB
3. 一个多态的反例
下面这种写法,不会触发多态。
3.1 代码示例
Base base = (Base)child;
base.PrintTestA();
base.PrintTestB();
从语法上看,Base base,本身就是定义了一个实体对象base,”= (Base)child”表示从child对象中将它的Base部分的成员变量的值拷贝到左边的base对象中。
(*pBase)已经从Base指针类型转为了Base类型,编译器依然会通过虚表指针来调用函数。所以,准确的讲,还是要看一个对象在初始化内存时,实际是什么类型的内存布局的。