代码世界C++程序员

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

2017-06-04  本文已影响109人  CodingCode

这篇文章继续分析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的虚函数表中定义有两个析构函数,不知道为什么。

上一篇 下一篇

猜你喜欢

热点阅读