深入理解c++多态的实现原理

本文实验都是在x86-64 架构的主机上运行,指针占8个字节。 

1.什么是多态

多态:允许不同的对象,对相同的消息产生不同的响应;c++中多态分为两种:一种是静态多态,主要指的是函数重载;另一种是动态多态,通过函数覆盖(重写)实现;本文主要讨论动态多态,以下统称为多态。

多态发生时机:使用基类指针或引用指向子类对象,且调用虚函数时,就会根据指针或引用指向的对象的实际类型,访问实际对象重写的虚函数。

2.多态如何实现(铺垫知识)

c++多态的实现依赖于虚函数,虚表指针和虚函数表;

2.1虚函数

虚函数是使用关键字virtual在基类中声明的成员函数。它允许派生类重写该函数,并在运行时根据对象的实际类型调用相应的函数。

2.2虚函数表

在编译阶段如果发现某个类中有虚函数,会在编译阶段为这个类生成一张唯一的虚函数表,虚函数表本质就是一个指针数组,存储RTTI类型信息,偏移量,和虚函数的实际访问地址(稍后做详细说明);程序运行时每一张虚函数表都会被加载到内存的.rodata区,使用同一个类创建出的对象都共享这同一张表

2.3虚表指针

一个类中,如果声明了虚函数,就会在这个对象的前8个字节存储一个指针(不考虑虚继承),指向虚函数表。

2.4.验证

2.4.1测试一

首先创建一个Base类,类中没有虚函数,使用gdb查看对象base的内存分布。

class Base{
public:
    int memberBase_;
    Base(int memberBase){
        memberBase_=memberBase;
    }
    void func1(){
        std::cout<<"func1 Base"<<std::endl;
    }
};
int main(){
    Base base(1);

    std::cout<< sizeof(base)<<std::endl;

};

输出结果:

size: 4

可以发现对象base确实只有一个int类型的成员变量,大小为4B。

2.4.2测试二

为Base添加两个虚函数,func1,func2;Derive继承类Base,并且只对方法func1进行覆盖。

#include <iostream>
class Base{
public:
    int memberBase_;
    Base(int memberBase){
        memberBase_=memberBase;
    }
    virtual void func1(){
        std::cout<<"func1 Base"<<std::endl;
    }
    virtual void func2(){
        std::cout<<"func2 Base"<<std::endl;
    }
};
class Derive :public Base{
public:
    int memberDerive_;
    Derive(int memberBase, int memberDerive) : Base(memberBase) {
        memberDerive_=memberDerive;
    }
    void func1(){
        std::cout<<"func1 Derive"<<std::endl;
    }

};
int main(){
    Base base(1);
    Derive derive(2,3);
    std::cout<<"sizeof base: "<<sizeof(base)<<std::endl;
    std::cout<<"sizeof derive: "<<sizeof(derive)<<std::endl;

};

 使用gdb查看对象的内存布局

可以明显的看出无论是base,还是继承自Base的derive对象,在起始部分都多存储了一个指针,指向了虚函数表,这就是虚表指针。验证了类中如果声明了虚函数,就会在这个对象的起始位置存储一个虚表指针

注意在虚函数指针的尖括号无论是Base还是Derive都有一个+16这里暂时按下不表;

2.4.3测试三

代码同测试二,使用gdb的info vtbl命令可以查看虚函数表的内存布局

首先解释一下输出结果,前面我们说过虚函数表本质就是一个指针数组,第一列的0和1就是虚函数地址在虚函数表中的下标 ;第二列展现的是虚函数表中虚函数的实际地址;第三列是注释它不存储在虚函数表中,虚函数表存储的内容仅仅是函数地址

可以看到base对象的两个虚函数都是来自它自己;而在derive的虚函数表中,由于Derive类重写了func1,因此在derive类的虚函数表中,存储的是自己重写的虚函数的地址;而对于func2由于派生类并没有重写,因此使用的还是基类的func2,可以发现二者的地址是一样的0x7ff6c3805560

2.4.4测试四

验证同一个类下的所有对象共享同一个虚函数表,类定义同测试二。

int main(){
    Base base1(1);
    Base base2(2);
    Derive derive1(3,4);
    Derive derive2(5,6);
    std::cout<<"end";
};

可以看出,相同的类的对象指向的确实是同一个虚函数表。 

现在可以回答刚才的那个问题:虚表指针的+16代表的意思,代表的是相对于虚表表头的偏移量,事实上虚函数表除了存储虚函数的指针以外,还需要存储额外的内容,RTTI运行类型信息指针,它指向的是对象的类型信息,即对象的实际类型信息,还要存储虚表指针相对于对象起始地址的偏移量,在不涉及虚继承的情况下该位置存储的就是0,即没有任何偏移,在对象的起始地址存储虚表指针。示意图如图所示。

3.从汇编剖析多态原理

当我们使用指针(或者引用)调用函数时,首先查看该函数是否为虚函数,如果不是虚函数,那么不涉及多态,直接在编译阶段确定要访问的函数地址如果是虚函数,那么首先去这个对象起始地址的前8个字节拿到虚表指针,进而访问虚函数表,编译器会根据访问的是哪个虚函数,进而访问虚函数表对应的下标,进而获得要访问的虚函数的地址

接下来将通过分析汇编指令,从汇编的角度看多态到底发生了什么。通过基类指针basePtr,指向派生类对象d,其中func1和func2是虚函数,func3不是虚函数;也就是说通过basePtr调用func1和func2会发生多态,调用func3不会发生多态。

#include <iostream>
class Base{
public:
    int memberBase_;
    Base(int memberBase){
        memberBase_=memberBase;
    }
    virtual void func1(){
        std::cout<<"func1 Base"<<std::endl;
    }
    virtual void func2(){
        std::cout<<"func2 Base"<<std::endl;
    }
    void func3(){
        std::cout<<"func3 Base"<<std::endl;
    }
};
class Derive :public Base{
public:
    int memberDerive_;
    Derive(int memberBase, int memberDerive) : Base(memberBase) {
        memberDerive_=memberDerive;
    }
    void func1(){
        std::cout<<"func1 Derive"<<std::endl;
    }

};
int main(){
    Derive d(1,2);
    Base* basePtr=&d;
    basePtr->func3();
    basePtr->func2();
    std::cout<<"end";
};

我们使用clion查看汇编代码,分析basePtr->func3()、basePtr->func2()这两个函数调用的汇编码,如图所示。

 

movq代表移动8B;movq -8(%rbp), %rax指令的含义是:将对象d的起始地址移动到rax寄存器中;当调用func3时,由于func3不是虚函数,因此编译阶段就可以确定调用的是哪个函数;145,146两行汇编的意思是将对象d的起始地址移动到rcx寄存器内,147行:调用func3。为什么调用函数还需使用对象d的地址呢,道理很简单,因为非静态成员方法的调用,都依赖一个对象

下面看basePtr->func2();

  • 148行:将对象地址移动到rax寄存器中;
  • 149行:将rax寄存器中存放的内存地址的前8B,移动到rax寄存器中,对象起始8B存的是vfptr,实际就是将虚表指针移动到rax寄存器中
  • 150行:将虚表指针+8,由于我们访问的是func2,它存储在虚函数表的第二个表项中,因此要+8;
  • 151行:将虚函数func2的地址存入rdx寄存器
  • 152、153行和刚才一样:由于调用的是成员方法,依赖对象,因此将对象地址移动到rcx寄存器中
  • 154行:调用rdx寄存器指向的函数func2

这就是动态绑定,只有当程序运行起来是才知道rdx寄存器中存储的内容,才知道要调用哪个虚函数

4.总结

使用基类指针或引用指向子类对象,且调用虚函数时,就会发生多态;多态的实现依赖于虚函数,虚表指针和虚函数表;使用指针(或者引用)调用函数时,首先查看该函数是否为虚函数,如果是虚函数,那么首先由对象起始地址的前8个字节拿到虚表指针,进而访问虚函数表,进而获得要访问的虚函数的地址。

  • 27
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值