C++虚函数表的内容分析

这篇文章继续分析C++虚函数表的内容,以及它的工作原理,即用户代码如何访问虚函数表的内容。

下面C++代码定义了一个类AAAA,main()函数new了一个对象,然后delete对象,我们按照调用顺序分析虚函数表的建立,关联等等操作。

#include <stdio.h>
#include <string>

class AAAA {
private:
    long l;
public:
    virtual void foo() {}
    virtual ~AAAA() {}
};

int main(int argc, char * argv[]) {
    AAAA * a = new AAAA();

    delete a;
    return 0;
}

从main()函数入口,主要有两条指令new一个AAAA对象,然后删除这个对象。

AAAA * a = new AAAA()

new指令生成的汇编指令如下:


    movl    $16, %edi
    call    _Znwm                         # operator new(unsigned long)
    movq    %rax, %rbx
    movq    %rbx, %rax
    movq    $0, (%rax)                #set instance buffer to 0
    movq    $0, 8(%rax)              # set instance buffer to 0
    movq    %rax, %rdi                # move instance pointer to %rdi for calling
    call    _ZN4AAAAC1Ev         # AAAA::AAAA()

主要有个三块功能,1. new一个16字节的内存,2. 内存初始化成0,3. 调用构造函数AAAA::AAAA(),即_ZN4AAAAC1Ev。
我们再看构造函数AAAA::AAAA()的代码:

_ZN4AAAAC1Ev:               # AAAA::AAAA()
    pushq   %rbp
    movq    %rsp, %rbp
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    $_ZTV4AAAA+16, (%rax)
    leave
    ret

在C++源代码里面,我们并没有为AAAA定义自己的构造函数,所以这个函数是缺省的构造函数,主要功能就一句话,把$_ZTV4AAAA+16的值赋值到对象实例的前8个字节(movq $_ZTV4AAAA+16, (%rax))。

再来看_ZTV4AAAA+16是个什么内容:

_ZTV4AAAA:                                              # vtable for AAAA
    .quad   0
    .quad   _ZTI4AAAA                               # typeinfo for AAAA
    .quad   _ZN4AAAA3fooEv
    .quad   _ZN4AAAAD1Ev
    .quad   _ZN4AAAAD0Ev
_ZTS4AAAA:                                             # typeinfo name for AAAA
    .string "4AAAA"
_ZTI4AAAA:                                              # typeinfo for AAAA
    .quad   _ZTVN10__cxxabiv117__class_type_infoE+16
    .quad   _ZTS4AAAA                             # typeinfo name for AAAA

上述代码都有编译器在翻译类AAAA的时候生成。我们看到_ZTV4AAAA是类AAAA的虚函数表地址,$_ZTV4AAAA+16指向的是虚函数AAAA:::foo()的地址;我们已经知道C++类对象内容的前八个字节是指向类虚函数表的指针,可是此时我们看到它并不是指向虚函数表首地址,而是指向首地址+16的一个偏移,为什么这样做呢?其实+16是第一个虚函数的地址,前面的16字节(+8字节指向类类型信息,+0我也不清楚其用处)保留属于C++类管理内部使用的,对用户而言可以隐藏,所以在使用者的角度看来,虚函数表就是按顺序从头开始排列的(+16偏移开始即可。

总结一句话,缺省构造函数就是把类的虚函数表地址写到类对象的前面8个字节地址。

下面我们看删除一个对象的函数

delete a;

delete指令生成汇编语言代码如下:

    movq    -24(%rbp), %rax
    movq    (%rax), %rax
    addq    $16, %rax
    movq    (%rax), %rdx
    movq    -24(%rbp), %rax
    movq    %rax, %rdi
    call    *%rdx

这段汇编代码的目的只有一个就是call到%rdx里面去,完成两件事,1.给%rdx找到正确的值,2.找到正确的函数参数。%rdx需要找到的值是析构函数的地址,参数当然是对象本身指针了。

从上述代码我们看到赋给%rdx的值是虚函数表+16的地址,看前面_ZTV4AAAA的定义,(+16)的地址就是指向第三个虚函数的地址,即_ZN4AAAAD0Ev


_ZN4AAAAD0Ev:           # AAAA::~AAAA()
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    %rax, %rdi
    call    _ZN4AAAAD1Ev
    movq    -8(%rbp), %rax
    movq    %rax, %rdi
    call    _ZdlPv
    leave
    ret

这个函数主要功能是调用另一个函数 _ZN4AAAAD1Ev

_ZN4AAAAD1Ev:           # AAAA::~AAAA()
    pushq   %rbp
    movq    %rsp, %rbp
    subq    $16, %rsp
    movq    %rdi, -8(%rbp)
    movq    -8(%rbp), %rax
    movq    $_ZTV4AAAA+16, (%rax)
    movq    -8(%rbp), %rax
    movq    %rax, %rdi
    call    _ZdlPv
    leave
    ret

函数_ZN4AAAAD1Ev是用户定义的析构函数,因为没有具体功能;也不清楚它具体要干什么,只看到最后它调用了一个delete函数。

上述代码可能比较复杂啰嗦,但是我们清楚了一个重要概念,即每一个多态类实例对象的起始地址都是一个指向虚函数表的指针,所有类的虚函数都在这个表中占用一列;这个地址的前面一个指针指向类的类型信息定义,从而从一个对象指针我们就能查到其类类型定义;这也是typeid和dyanmic_cast能够工作的原理。

1.jpg

最后说明一点在类_ZTV4AAAA的虚函数表中定义有两个析构函数,不知道为什么。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • C++虚函数 C++虚函数是多态性实现的重要方式,当某个虚函数通过指针或者引用调用时,编译器产生的代码直到运行时才...
    小白将阅读 5,689评论 4 19
  • 1.面向对象的程序设计思想是什么? 答:把数据结构和对数据结构进行操作的方法封装形成一个个的对象。 2.什么是类?...
    少帅yangjie阅读 10,456评论 0 14
  • 1. 结构体和共同体的区别。 定义: 结构体struct:把不同类型的数据组合成一个整体,自定义类型。共同体uni...
    breakfy阅读 6,551评论 0 22
  • 一个博客,这个博客记录了他读这本书的笔记,总结得不错。《深度探索C++对象模型》笔记汇总 1. C++对象模型与内...
    Mr希灵阅读 11,036评论 0 13
  • 厨艺这种东西还真需要熟能生巧,走不来捷径啊。 学习吧,方法真的是可以很多,执行过程中屡屡受挫,这些年越发不能控制自...
    好大一只虫阅读 1,423评论 0 0

友情链接更多精彩内容