为什么fdump-class-hierarchy为虚函数提供了两个指针int vtable

时间:2018-05-06 00:25:39

标签: c++ dynamic-cast vtable typeid virtual-destructor

以下课程,

class A
{
public:

    char VarA;
    int VarB;

    virtual ~A(){}
};

g ++ fdump-class-hierarchy为我提供了Vtable,

Vtable for A
A::_ZTV1A: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI1A)
16    (int (*)(...))A::~A
24    (int (*)(...))A::~A

但我不明白: 1.两个第一指针是什么? 2.为什么虚拟析构函数有两个指针?

谢谢!

1 个答案:

答案 0 :(得分:0)

让我们尝试以下演示代码:

class A
{
public:

    char VarA;
    int VarB;

    virtual ~A();
};

#include <cstdio>

A::~A() { 
    std::printf("A::~A()\n");
}

#include <typeinfo>

void *complete_object_addr(A &ref) {
    return dynamic_cast<void*> (&ref);
}

const std::type_info& get_typeinfo(A &ref) {
    return typeid(ref);
}

void explicit_destructor_call(A *p) {
    p->~A();
}

void delete_object(A *p) {
    delete p;
}

#include <memory>

void create_object (A *p) {
    new (p) A;
}
Godbolt

vtable

让我们从构造函数调用开始

void create_object (A *p) {
    new (p) A;
}

查看vptr和vtable在哪里:

create_object(A*):
        movq    $vtable for A+16, (%rdi)
        ret

A*参数位于rdi中,vptr在对象中的偏移量为0,因此*(void*)p给出了vptr。该vtable生成为:

vtable for A:
        .quad   0
        .quad   typeinfo for A
        .quad   A::~A() [complete object destructor]
        .quad   A::~A() [deleting destructor]

我们看到vptr指向内部(而不是vtable的开头):.quad意味着vptr指向第三个元素A::~A() [complete object destructor]的8个字节。

这种输出vtable的方法比您所问的要清晰得多:有两个可以虚拟地调用的析构函数:

  • “ [complete object destructor]”(销毁完整对象),
  • “ [删除析构函数]”以删除完整的对象。

虚拟析构函数

实际上,explicit_destructor_call(A*)的代码定义为

void explicit_destructor_call(A *p) {
    p->~A();
}

显示对函数指针vptr[0]的调用。

explicit_destructor_call(A*):
        movq    (%rdi), %rax
        jmp     *(%rax)

对虚拟函数的调用为(p->vptr)(p);请注意,传递this参数在生成的代码中是隐含的,就像在寄存器中一样。

这里有一个窍门,您需要关闭汇编指令过滤器才能看到它:

.text
.size   A::~A() [base object destructor], .-A::~A() [base object destructor]
.globl  A::~A() [complete object destructor]
.set    A::~A() [complete object destructor],A::~A() [base object destructor]

我不习惯使用这些指令,但这肯定意味着A::~A() [complete object destructor]实际上是A::~A() [base object destructor],即:

.LC0:
        .string "A::~A()"
A::~A() [base object destructor]:
        movq    $vtable for A+16, (%rdi)
        movl    $.LC0, %edi
        jmp     puts
  • 首先将vptr设置为构造函数中的方式:将*this的动态类型重置为A,这仅对销毁基类子对象有用,而对销毁完整对象无用
  • 然后调用puts("A::~A()");(这表明printf规范字符串在可能的编译时进行解释)。

函数delete_object(A*)定义为

void delete_object(A *p) {
    delete p;
}

编译为

delete_object(A*):
        testq   %rdi, %rdi
        je      .L8
        movq    (%rdi), %rax
        jmp     *8(%rax)

这稍微复杂一点:需要测试p!=0,因为当delete p;为空时,p是合法的。如果p不为零,则代码跳转到(char*)vptr + 8,这是vtable中的下一项:vptr[1]

该析构函数编译为:

A::~A() [deleting destructor]:
        pushq   %rbx
        movq    %rdi, %rbx
        movq    $vtable for A+16, (%rdi)
        movl    $.LC0, %edi
        call    puts
        movq    %rbx, %rdi
        movl    $16, %esi
        popq    %rbx
        jmp     operator delete(void*, unsigned long)

首先,我们将动态类型重置为A。 (我认为实际上并不需要。)然后,像在“销毁对象”析构函数中那样输出文本,然后调用operator delete(this,sizeof(A));

RTTI:typeiddynamic_cast

获取typeid(lvalue of A)的值非常简单;

const std::type_info& get_typeinfo(A &ref) {
    return typeid(ref);
}

编译为

get_typeinfo(A&):
        movq    (%rdi), %rax
        movq    -8(%rax), %rax
        ret

我们看到vptr[-1]被读取并返回。

最后,通过dynamic_cast<void*>

获取最派生对象的地址
void *complete_object_addr(A &ref) {
    return dynamic_cast<void*> (&ref);
}

非常简单:

complete_object_addr(A&):
        movq    (%rdi), %rax
        addq    -16(%rax), %rdi
        movq    %rdi, %rax
        ret

vptr[-2]之后偏移this的最派生对象(这些偏移几乎总是负数)。