虚继承和基类中的空虚函数表

3
这是一段代码:

有这样一段代码:

#include <iostream>

class Base
{
   int x;
};

class Derived : virtual public Base
{
   int y;
};

int main()
{
    std::cout << sizeof(Derived) << std::endl; // prints 12
    return 0;   
}

我了解到,当某个类进行虚拟继承时,会为派生类创建一个空的虚函数表,因此内存布局如下:

Derived::ptr to empty vtable
Derived::y
Base::x

它有12个字节。问题是 - 如果没有任何虚拟方法,这个空的 vtable的目的是什么,以及它如何被使用?


3个回答

5

Derived 需要知道 Base 子对象的位置。在虚继承中,基类的相对位置不固定于派生类的位置:它可以位于完整对象的任何位置。

考虑一个更典型的菱形继承示例。

struct A
{
    int a;
};

struct B1 : virtual A
{
    int b1;
};

struct B2 : virtual A
{
    int b2;
};

struct C : B1, B2
{
    int c;
};

在这里,B1B2均从A派生而来,因此在C中,只有一个A子对象。为了能够访问a成员变量或A的其他成员(如果我们定义了它们),B1B2都需要知道如何找到该A子对象。
在这种情况下,vtable的作用就是这样: B1B2将拥有包含A子对象偏移量的vtable。
为了演示编译器可能执行的上述菱形继承示例的实现方法,请考虑以下类布局和虚拟表格,由Visual C++ 11 Developer Preview生成。
class A size(4):
        +---
 0      | a
        +---

class B1        size(12):
        +---
 0      | {vbptr}
 4      | b1
        +---
        +--- (virtual base A)
 8      | a
        +---

class B2        size(12):
        +---
 0      | {vbptr}
 4      | b2
        +---
        +--- (virtual base A)
 8      | a
        +---

class C size(24):
        +---
        | +--- (base class B1)
 0      | | {vbptr}
 4      | | b1
        | +---
        | +--- (base class B2)
 8      | | {vbptr}
12      | | b2
        | +---
16      | c
        +---
        +--- (virtual base A)
20      | a
        +---

以下是相关的虚表:

B1::$vbtable@:
 0      | 0
 1      | 8 (B1d(B1+0)A)

B2::$vbtable@:
 0      | 0
 1      | 8 (B2d(B2+0)A)

C::$vbtable@B1@:
 0      | 0
 1      | 20 (Cd(B1+0)A)

C::$vbtable@B2@:
 0      | 0
 1      | 12 (Cd(B2+0)A)

请注意,偏移量是相对于虚表地址的,另外请注意,在 C 对象的 B1B2 子对象生成的两个虚表中,偏移量不同。
(还要注意,这完全是实现细节——其他编译器可能以不同的方式实现虚函数和基类。此示例演示了一种实现方式,而且这种方式非常普遍。)

偏移量是相对于虚函数表的地址而言的,不是相对于对象的。 - curiousguy
为什么每个虚拟基表的第一个条目总是0?实际偏移量始终在第二个条目上。那么第一个条目的目的是什么? - savram

1
为了实现虚函数,C++使用一种特殊的“后期绑定”形式,称为“虚表”。虚表是一个查找函数的表,以动态/后期绑定方式解析函数调用。虚表有时被称为其他名称,如“vtable”,“虚函数表”,“虚方法表”或“分派表”。
虚表实际上非常简单。首先,每个使用虚函数(或从使用虚函数的类派生的类)的类都会被赋予自己的虚表。这个表只是一个静态数组,在编译时由编译器设置。虚表包含每个可以由该类对象调用的虚函数的一个条目。此表中的每个条目只是指向该类可访问的最派生函数的函数指针。
  • 每个使用虚函数(或从使用虚函数的类派生的类)的类都会被赋予自己的虚表作为秘密数据成员。
  • 这个表是由编译器在编译时设置的。
  • 虚表为每个可以由该类对象调用的虚函数包含一个函数指针条目。
  • 虚表存储纯虚函数的NULL指针。
即使是具有虚基类的类,也会创建虚表。在这种情况下,虚表除了指向基类的共享实例指针外,还包括指向类的虚函数(如果有)的指针。

虚函数表无法存储指向实例的指针,因为对于具有给定类型的每个对象都是共同的;它只能存储偏移量。 - curiousguy

0
如果您执行dynamic_cast(ptr_to_obj),它将使用vtable指针来确定ptr_to_obj是否引用Derived。每个涉及虚方法或继承的类都需要一个vtable,并且每个类都需要不同的vtable以支持dynamic_cast<>。即使它不包含任何指向方法的指针,它仍然用于识别对象的类型。

@james-mcnellis的评论说明了为什么这个虚函数表不会是空的。我的回复展示了即使虚函数表本来应该是空的,或者与基类相同,你为什么仍然需要一个虚函数表。 - Bob Kerns

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接