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

· 浏览次数 : 2

小编点评

**virtual_func1虚函数** 1. 从Base虚基类类型中继承来的virtual_func1虚函数存在于Base虚基类的虚函数表中。 2. 在调用virtual_func1虚函数之前,先要进行this指针的调整,让this指针指向Base子对象的起始地址。 3. 通过Base虚基类类型的指针调用Derived类的虚函数通过Base虚基类类型的指针调用Derived类的虚析构函数和virtual_func2虚函数。 **virtual_func2虚函数** 1. 从Base虚基类类型中继承来的virtual_func2虚函数存在于Base虚基类的虚函数表中。 2. 在调用virtual_func2虚函数之前,先要进行this指针的调整,让this指针指向Base子对象的起始地址。 3. 通过Base类型指针调用Derived类的虚函数通过Base虚基类类型的指针调用Derived类的虚析构函数和virtual_func1虚函数。

正文

“深度解读《深度探索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爱分享

与深度解读《深度探索C++对象模型》之C++虚函数实现分析(三)相似的内容:

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

本系列深入分析编译器对于C++虚函数的底层实现,最后分析C++在多态的情况下的性能是否有受影响,多态究竟有多大的性能损失。

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

本系列深入分析编译器对于C++虚函数的底层实现,最后分析C++在多态的情况下的性能是否有受影响,多态究竟有多大的性能损失。

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

本系列深入分析编译器对于C++虚函数的底层实现,最后分析C++在多态的情况下的性能是否有受影响,多态究竟有多大的性能损失。

深度解读《深度探索C++对象模型》之数据成员的存取效率分析(二)

C++对象在经过类的封装后,存取对象中的数据成员的效率是否相比C语言的结构体访问效率要低下?本篇将从C++类的不同定义形式来一一分析C++对象的数据成员的访问在编译器中是如何实现的,以及它们的存取效率如何?

深度解读《深度探索C++对象模型》之默认构造函数

C++的默认构造函数的作用是什么?什么时候会需要一个默认构造函数,以及默认构造函数从哪里来?这篇文章将从编译器的角度来分析这个问题。

Python生成器深度解析:构建强大的数据处理管道

# 前言 生成器是Python的一种核心特性,允许我们在请求新元素时再生成这些元素,而不是在开始时就生成所有元素。它在处理大规模数据集、实现节省内存的算法和构建复杂的迭代器模式等多种情况下都有着广泛的应用。在本篇文章中,我们将从理论和实践两方面来探索Python生成器的深度用法。 ## 生成器的定义

Django ORM:最全面的数据库处理指南

**深度探讨Django ORM的概念、基础使用、进阶操作以及详细解析在实际使用中如何处理数据库操作。同时,我们还讨论了模型深入理解,如何进行CRUD操作,并且深化理解到数据库迁移等高级主题。为了全面解读Django ORM,我们也讨论了其存在的不足,并对其未来发展进行了展望。这篇文章旨在帮助读者全

编织人工智能:机器学习发展历史与关键技术全解析

>本文全面回顾了机器学习的发展历史,从早期的基本算法到当代的深度学习模型,再到未来的可解释AI和伦理考虑。文章深入探讨了各个时期的关键技术和理念,揭示了机器学习在不同领域的广泛应用和潜力。最后,总结部分强调了机器学习作为一种思维方式和解决问题的工具,呼吁所有参与者共同探索更智能、更可持续的未来,同时

OpenCV实战:从图像处理到深度学习的全面指南

> 本文深入浅出地探讨了OpenCV库在图像处理和深度学习中的应用。从基本概念和操作,到复杂的图像变换和深度学习模型的使用,文章以详尽的代码和解释,带领大家步入OpenCV的实战世界。 # 1. OpenCV简介 ## 什么是OpenCV? ![file](https://img2023.cnblo

深入解析React DnD拖拽原理,轻松掌握拖放技巧!

>我们是[袋鼠云数栈 UED 团队](http://ued.dtstack.cn/),致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。。 >本文作者:霁明 # 一、背景 ## 1、业务背景 业务中会有一些需要实现拖拽的场景,尤其是偏视觉方向以及移动端