是否存在空虚表?

Can an empty virtual table exist?

本文关键字:虚表 空虚 存在 是否      更新时间:2023-10-16
#include <iostream>
using namespace std;
class Z
{
public:
    int a;
    virtual void x () {}
};
class Y : public Z
{
public:
    int a;
};
int main() 
{
    cout << "nZ: "  << sizeof (Z);
    cout << "nY: "  << sizeof (Y);
} 

因为Y继承了Z,所以它也有一个虚表。很好。但是,它没有虚函数,那么Y的虚表的内容是什么呢?
会是空的吗?

这完全依赖于编译器。当我强制实例化YZ时,g++ 4.4.5YZ产生两个不同的虚拟表,它们具有相同的大小。

两个表指向相同的x(),但指向不同的typeinfo结构体:

;=== Z's virtual table ===
_ZTV1Z:
        .quad   0
        .quad   _ZTI1Z     ; Z's type info
        .quad   _ZN1Z5xEv  ; x()
_ZTI1Z:
        ; Z's type info (omitted for brevity)
;=== Y's virtual table ===
_ZTV1Y:
        .quad   0
        .quad   _ZTI1Y     ; Y's type info
        .quad   _ZN1Z5xEv  ; x()
_ZTI1Y:
        ; Y's type info (omitted for brevity)

在你发布的例子中,默认情况下GCC将完全优化掉虚表。因为它只有一个翻译单元,所有内容对它都是可见的,所以这是可能的。

我把你的例子改成:

#include <iostream>
using namespace std;
class Z
{
public:
    int a;
    virtual void x () const {}
};
class Y : public Z
{
public:
    int a;
};
int main()
{
    Y y;
    const Z& z1=y;
    const Z& z2=Z();
    z1.x(),z2.x();
    cout << "nZ: "  << sizeof (Z);
    cout << "nY: "  << sizeof (Y);
}

在本例中,输出中生成了一个虚值表:

nm a.out|c++filt|grep -i vtable
08048880 V vtable for Y
08048890 V vtable for Z
0804a040 V vtable for __cxxabiv1::__class_type_info@@CXXABI_1.3
0804a120 V vtable for __cxxabiv1::__si_class_type_info@@CXXABI_1.3

如果我们用-S生成程序集,那么我们可以找到构造函数(在我的系统上分别被修改为_ZN1ZC2Ev_ZN1YC2Ev)。它们负责设置虚表(_ZTV1Z_ZTV1Y):

Z:

_ZN1ZC2Ev:
.LFB970:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        movl    8(%ebp), %eax
        movl    $_ZTV1Z+8, (%eax)
        popl    %ebp
        .cfi_def_cfa 4, 4
        .cfi_restore 5
        ret

And Y:

_ZN1YC2Ev:
.LFB972:
        .cfi_startproc
        pushl   %ebp
        .cfi_def_cfa_offset 8
        .cfi_offset 5, -8
        movl    %esp, %ebp
        .cfi_def_cfa_register 5
        subl    $24, %esp
        movl    8(%ebp), %eax
        movl    %eax, (%esp)
        call    _ZN1ZC2Ev
        movl    8(%ebp), %eax
        movl    $_ZTV1Y+8, (%eax)
        leave
        .cfi_restore 5
        .cfi_def_cfa 4, 4
        ret
这里有趣的是,在两个构造函数中放入虚表的内容本质上是相同的。

在一般编译器实现中,Y的虚表将具有与Z

相同的表项。

这些细节已经在其他答案中添加了,但是从一个非常高级的角度来看,您必须认为派生类型Y确实继承了Z的所有虚函数,它只是没有为它们中的任何一个提供覆盖(好吧,对于单个)。

整个虚函数表的思想是,所有从该基基派生的类型都有一个与兼容的表。当编译器需要找到要调用的虚方法的特定实现时,它知道它可以依赖于存在的表,并且表的元素是指向该特定对象的方法的final-overrider的指针,即使final-overrider碰巧是第一个也是唯一的重写(如您的示例)。