掘金 后端 ( ) • 2024-05-16 13:46

theme: github highlight: dark

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