首页 > 编程语言 >深度解读《深度探索C++对象模型》之C++虚函数实现分析(三)

深度解读《深度探索C++对象模型》之C++虚函数实现分析(三)

时间:2024-05-16 12:07:40浏览次数:25  
标签:func2 函数 基类 Derived virtual 解读 Base C++ 深度

“深度解读《深度探索C++对象模型》”系列已经在CSDN上和我的公众号上更新完毕,请有需要的同学移步到我的CSDN主页里去阅读,主页地址:https://blog.csdn.net/iShare_Carlos?spm=1010.2135.3001.5421
或者敬请关注我的公众号:iShare爱分享

前面两篇请从这里阅读:
深度解读《深度探索C++对象模型》之C++虚函数实现分析(一)
深度解读《深度探索C++对象模型》之C++虚函数实现分析(二)

虚继承情况下的虚函数和多态的实现分析

虚继承如果再加上多重继承关系,或者具有两层以上的虚继承关系,那么编译器对于虚函数的支持简直像进了迷宫一样让人眼花缭乱,它们的关系让人扑朔迷离。其实在实际的应用中很少会出现这样的设计,也不建议这样做。我们还是以一个较为常用的只有一层的虚继承关系的例子来讲解对于虚函数的支持,如以下的例子:

#include <cstdio>

class Base {
public:
    virtual ~Base() = default;
    virtual void virtual_func1() { printf("%s\n", __PRETTY_FUNCTION__); }
    virtual void virtual_func2() { printf("%s\n", __PRETTY_FUNCTION__); }
    int b = 0;
};
class Derived: virtual public Base {
public:
    virtual ~Derived() = default;
    void virtual_func2() override { printf("%s\n", __PRETTY_FUNCTION__); }
    virtual void virtual_func3()  { printf("%s\n", __PRETTY_FUNCTION__); }
    int d = 0;
};

int main() {
    Derived* pd = new Derived;
    pd->virtual_func1();
    pd->virtual_func2();
    pd->virtual_func3();
    Base* pb = pd;
    pb->virtual_func1();
    pb->virtual_func2();
    delete pd;
    return 0;
}

上面的代码中继承关系虽然只是单一继承,但由于是虚继承,所以它不像普通的单继承那样,基类的子类部分和对象的起始地址是对齐的,虚函数表也共用同一个,由于虚继承的关系,虚基类的子类部分是共享的,一般编译器的实现会把它放到对象布局的最尾端,即在所有具体继承的子对象和子类之后,也不和任何子对象共用虚函数表,它自己单独拥有一个虚函数表。所以上面的代码编译器将会产生两个虚函数表,一个是Derived子类的,一个是Base虚基类的,只不过编译器把两个表合并在一起,两个子对象(Derived和Base)的虚函数表指针被设置指向不同的偏移地址,看看上面代码对应的汇编代码中的虚函数表:

vtable for Derived:
    .quad   16
    .quad   0
    .quad   typeinfo for Derived
    .quad   Derived::~Derived() [complete object destructor]
    .quad   Derived::~Derived() [deleting destructor]
    .quad   Derived::virtual_func2()
    .quad   Derived::virtual_func3()
    .quad   -16
    .quad   0
    .quad   -16
    .quad   -16
    .quad   typeinfo for Derived
    .quad   virtual thunk to Derived::~Derived() [complete object destructor]
    .quad   virtual thunk to Derived::~Derived() [deleting destructor]
    .quad   Base::virtual_func1()
    .quad   virtual thunk to Derived::virtual_func2()

Derived对象的虚函数表被设置指向上面的第5行的位置,Base虚基类的虚函数表被设置指向第14行的位置,这些事情都是编译器在默认析构函数中生成的代码来完成的,具体的分析可以见另外一篇文章《编译器背后的行为之默认构造函数》。因为虚继承的存在,上面的表中除了支持多态的虚函数和RTTI信息外,还包含了支持虚继承的信息,主要就是一些正负偏移值,用来在有需要时调整this指针,如第2行的16就是从Derived对象的起始地址调整到Base虚基类子对象的起始地址,第9到12行的-16用于从Base虚基类子对象调整回Derived对象的起始地址。上面部分是主表,下面部分是次表,主表中是Derived类定义的虚函数:虚析构函数、virtual_func2和virtual_func3两个虚函数,次表是从Base虚基类继承而来的虚函数,包括了虚析构函数、virtual_func1和virtual_func2两个虚函数,其中虚析构函数和virtual_func2虚函数在Derived类中进行了改写,所以这里存放的不是真正的虚函数实例的地址,而是指向thunk技术实现的一段汇编代码,汇编代码里会跳转到真实的虚函数实例中执行。

虚继承下支持虚函数的困难点主要在于两方面:一个是通过Derived类型的指针调用Base虚基类中的虚函数;另一个是通过Base虚基类类型的指针调用Derived类的虚函数。它们的调用关系跟多重继承下处理第二及后继基类的方式很相似,下面我们以这两点分别来讲解。

  • 通过Derived类型的指针调用Base虚基类中的虚函数

在上面C++代码中的第20到22行的三行调用中,对virtual_func2和virtual_func3虚函数的调用,因为这两个虚函数存在于Derived类的虚函数表中,所以对这两个的调用采用的是常规的调用方法。对virtual_func1虚函数的调用,因为virtual_func1虚函数是从Base虚基类继承来的且在Derived类中没有进行改写,因此它只存在于Base虚基类的虚函数表中,调用它之前先要进行this指针的调整,让this指针指向Base子对象的起始地址,再通过Base子对象的虚函数表指针来寻址到它的虚函数表,并调用对应的虚函数,下面是它的汇编代码:

mov     rax, qword ptr [rbp - 16]
mov     rcx, qword ptr [rax]
mov     rcx, qword ptr [rcx - 24]
mov     rdi, rax
add     rdi, rcx
mov     rax, qword ptr [rax + rcx]
call    qword ptr [rax + 16]

[rbp - 16]栈空间存放的是Derived对象的起始地址,对其取值即是虚函数表指针(如不熟悉请参考《C++对象封装后的内存布局》),它指向的是Derived类的虚函数表的起始地址,也即是上表中的第5的位置,[rcx - 24]的意思是往上偏移24字节并取值,往上偏移24字节即指向了表的开头位置,它的值是16,这个值就是上面介绍的用于支持虚继承调整this指针的作用,然后上面汇编代码的第4、5行把它加到rdi上,rdi寄存器存放的是Derived对象的起始地址,rdi寄存器(作为this指针)也将作为第7行调用虚函数时的参数。第6行的[rax + rcx]的意思是Derived对象的起始地址加上16偏移值然后取值,它是Base子对象的虚函数表指针(指向上表中的第14行),然后在第7行代码的调用时再加上16的偏移值即是virtual_func1虚函数对应的地址,即上表中的第16行。

  • 通过Base虚基类类型的指针调用Derived类的虚函数

通过Base虚基类类型的指针调用Derived类的虚析构函数和virtual_func2虚函数,采用的是相同的实现方法,即thunk技术。所以放在一起来讲,先来看下它们的汇编代码:

virtual thunk to Derived::~Derived() [deleting destructor]:	# @virtual thunk to Derived::~Derived() [deleting destructor]
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     rdi, qword ptr [rbp - 8]
    mov     rax, qword ptr [rdi]
    mov     rax, qword ptr [rax - 24]
    add     rdi, rax
    pop     rbp
    jmp     Derived::~Derived() [deleting destructor] # TAILCALL
# 另一个虚析构函数的代码差不多,这里省略

virtual thunk to Derived::virtual_func2():	# @virtual thunk to Derived::virtual_func2()
    push    rbp
    mov     rbp, rsp
    mov     qword ptr [rbp - 8], rdi
    mov     rdi, qword ptr [rbp - 8]
    mov     rax, qword ptr [rdi]
    mov     rax, qword ptr [rax - 40]
    add     rdi, rax
    pop     rbp
    jmp     Derived::virtual_func2()    # TAILCALL

通过Base类型的指针来调用Derived类的虚析构函数的场景是:Base类型的指针指向Derived的对象,然后调用了delete函数释放这个对象,这时调用的是在Base子对象的虚函数表中的虚析构函数,它是thunk技术实现的一段汇编代码。virtual_func2虚函数定义在Derived类中,又是对Base虚基类中的virtual_func2虚函数的改写,所以存在于两个虚函数表中,但实际的函数实例只有一个,在Base虚基类的虚函数表中存放的是thunk技术实现的一段汇编代码。

上面的两个函数都是thunk技术生成的汇编代码,代码的内容基本一样,只是在最后一行跳转到不同的函数中去执行。首先将this指针(保存在rdi寄存器中,这时指向Base子对象的地址)保存到[rbp - 8]的栈空间中,然后取值并保存到rax寄存器中,这里取到的值是Base子对象中的虚函数表指针,即指向上表中第14行的位置,然后减去24(或40)的偏移量并取值,这两处的值都是-16,然后加上rdi中,rdi保存的是Base子对象的地址,向下偏移16字节后回到Derived对象的起始地址,然后跳转到相应的函数中去执行。

“深度解读《深度探索C++对象模型》”系列已经在CSDN上和我的公众号上更新完毕,请有需要的同学移步到我的CSDN主页里去阅读,主页地址:https://blog.csdn.net/iShare_Carlos?spm=1010.2135.3001.5421
或者敬请关注我的公众号:iShare爱分享

标签:func2,函数,基类,Derived,virtual,解读,Base,C++,深度
From: https://www.cnblogs.com/isharetech/p/18195731

相关文章

  • 《Effective C++》第三版-5. 实现(Implementations)
    目录条款26:尽可能延后变量定义式的出现时间(Postponevariabledefinitionsaslongaspossible)条款27:尽量少做转型动作(Minimizecasting)条款28:避免返回handles指向对象内部成分(Avoidreturning“handles”toobjectinternals)条款29:为“异常安全”而努力是值得的(Striveforexc......
  • C++:自定义异常
    #include<iostream>#include<stdexcept>//自定义异常类classMyException:publicstd::exception{public://重写what()函数以提供异常的描述,const表示函数不会改变类的成员变量,noexcept表示不会抛出异常constchar*what()constnoexceptoverride{......
  • C++ 对象池
    对象池概念对象池模式(ObjectPoolPattern),是创建型设计模式的一种,将对象预先创建并初始化后放入对象池中,对象提供者就能利用已有的对象来处理请求,减少频繁创建对象所占用的内存空间和初始化时间。对象池的用户可以从池子中取得对象,对其进行操作处理,并在不需要时归还给池子而非......
  • C++基础篇
    输入输出流iostream向流写入数据<<运算符<<运算符接受两个运算对象,此运算符将给定的值写到给定的ostream对象中:左侧:运算对象为ostream对象,如cout、cerr、clog右侧:运算对象是要打印的值输出结果:写入给定值的那个ostream对象,即此运算符返回其左侧的运算对象。表达式等价于:(std......
  • msvc 获取c++类内存布局 /d1 reportAllClassLayout
     visualstudio配置获取所有类内存布局/d1reportAllClassLayout或者指定类/d1reportSingleClassLayoutXXXclass  编译时输出:     ps:https://www.openrce.org/articles/full_view/23   【原文地址】https://blog.csdn.net/qq_29542611/article......
  • QT5.0_TensorBoard相关曲线解读
    TensorBoard生成的各种可视化图表可以帮助你解读和分析训练过程中的不同指标。以下是对一些常见图表的解释:1.损失曲线(LossCurve)损失曲线显示了训练过程中的损失(loss)随时间的变化情况。一般会有两条曲线:训练损失和验证损失。训练损失(TrainingLoss):反映模型在训练数据上的表......
  • 咳嗽检测深度神经网络算法
    具体的软硬件实现点击http://mcu-ai.com/MCU-AI技术网页_MCU-AI咳嗽检测是一种很有前途的检测呼吸道疾病各种病理严重程度的技术。自动咳嗽检测系统的开发将成为早期诊断的最佳跟踪工具。长期以患者为中心的远程咳嗽严重程度监测将改变医疗基础设施的游戏规则,因为在过去几十年......
  • linux下使用c++模拟下载进度
    #include<iostream>#include<iomanip>#include<chrono>#include<thread>voidshowProgressBar(doubleprogress){constintbarWidth=70;std::cout<<"\r[";intpos=static_cast<int>(barWid......
  • 利用深度循环神经网络对心电图降噪
    具体的软硬件实现点击http://mcu-ai.com/MCU-AI技术网页_MCU-AI我们提出了一种利用由长短期记忆(LSTM)单元构建的深度循环神经网络来降噪心电图信号(ECG)的新方法。该网络使用动态模型ECG生成的合成数据进行预训练,并使用来自PhysionetPDB心电图信号数据库的真实数......
  • C++封装dll(__cdecl和__stdcall)
    【1】使用__stdcall还需要添加def文件编译,使用工具DEPENDS.EXE打开dll文件成功。【2】使用__cdecl直接编译即可,不需要导入def文件......