C++多态底层原理分析

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,”= (Base)child”表示从child对象中将它的Base部分的成员变量的值拷贝到左边的base对象中。

  • 从反汇编看,push是参数压栈,这里其实是调用的Base的拷贝构造函数,不是无参构造函数。

  • 函数调用也是直接调用的Base函数地址,没有通过虚表

  • 简单理解,指针类型的调用才会触发多态,但也不全对,比如下面这样写:

(*pBase)已经从Base指针类型转为了Base类型,编译器依然会通过虚表指针来调用函数。所以,准确的讲,还是要看一个对象在初始化内存时,实际是什么类型的内存布局的。

建站  ·  开发  ·  读书  ·  教程
↑长按或扫码关注“编程之海”公众号↑
↑长按或扫码关注“编程之海”公众号↑
0 0 投票数
文章评分
订阅评论
提醒
guest
0 评论
内联反馈
查看所有评论

0

0

407

0
希望看到您的想法,请您发表评论x