vptr - 虚函数表

3

我还有一些不太明白。

我声明每个类时,都会有一个指向该类虚表的隐藏vptr成员。

假设我有以下声明:

class BASE
{
    virtual_table* vptr; //that's hidden of course , just stating the obvious
    virtual void foo();
}

class DERIVED : public BASE
{
   virtual_table* vptr; //that's hidden of course also
   virtual void foo();
   virtual void cho();
}

首先,我想了解一些事情,派生类和基类的vptr成员名称是否相同?

其次,在以下情况下会发生什么:

base* basic = new derived();

我明白了,基本变量获取派生类的vptr,但这是如何发生的?因为通常在进行转换时,派生类的基类部分(包括基类的vptr)应该被分配给基本变量,而不是派生类的vptr。如果两个类中有相同名称的变量,可能会有所不同,我不确定。
第三个也是最后一个问题: 当我有
 base* basic = new derived();

即使它是虚函数,是否有一种调用基类成员函数的基本方法?

谢谢


参见:https://dev59.com/e0rSa4cB1Zd3GeqPUTN0 - MaR
8个回答

3

首先,是的,它是同一个成员变量。它在运行基类构造函数时自动分配第一次,并在运行派生类构造函数时分配第二次。(在默认空构造函数的情况下,基类的无用赋值被优化掉了。)

其次,没有真正的转换。事实上,派生可以被称为“是一个”关系。在这种情况下,派生“是一个”基类。如果你考虑派生对象的前几个字节,它们与基类对象的前几个字节具有相同的含义。

第三,你可以按如下方式调用基本成员函数:basic->base::foo();


你的意思是 basic->base::foo(); - Janusz Lenar
如果基类的构造函数不为空,那么在基类构造时初始化vptr是必要的吗? - choxsword

3
首先,虚拟表不是C++标准的一部分。C++编译器可以自由地实现虚函数。通常情况下,它们会使用虚拟表,但在这种情况下,它们可以以任何适当的方式实现。
在大多数常见的实现中,basic并没有得到derived的vptr;basic的*vptr将指向derived的vtable,这并不是完全相同的。basic只是一个指针,被指向的对象具有vptr。没有涉及任何转换。无论如何,vptr只是一个内部名称,用于实现细节。没有真正的类成员叫做vptr。
而且,你总是可以通过限定类名来调用任何基类函数(在你的情况下,basic->BASE::foo())。
更新:仅供记录,我已经尝试在VC++2008中创建一个带有指针__vfptr的类(这是该编译器中vptr的内部名称),它按预期工作,尽管调试器对变量名称有点困惑。

1

vtable(也就是虚函数表)会根据 DERIVED 是否覆盖了 BASE 中至少一个虚函数的实现而指向不同的地址。每个 BASE 的实例将拥有相同的 vtable 指针,DERIVED 同理。

当你执行以下操作时:

someobject->foo();

这被翻译成:

someobject->vtable[COMPILER_GENERATED_OFFET_FOR_FOO]();

vtable 是一个函数指针数组,其中某些虚函数(例如 foo)的偏移量对于类层次结构中的所有类都是相同的。


“will point” 这个词有点过于强烈了,其实并不一定需要这样。编译器可以折叠虚函数表:如果至少有一个类没有重写任何虚函数,那么它的虚函数表开头将与基类的虚函数表相同。结果是你不能使用该值来标识一个对象。 - MSalters

1
关于第二个问题。(这不是标准中说明的,并且可以用其他方式实现,因此只需要从中提取一般思想。对于单继承层次结构来说,它也过于简化,多继承会使一切变得更加复杂。)
派生对象和基础对象的内存布局完全重合,只要基础对象的大小(包括编译器注入的任何数据)。这意味着一个具有“基础”类型的指针,实际上指向“派生”,实际上将指向可以解释为“基础”对象的一段内存,即使内容(vptr值)不同,布局也相同。
base          derived

base_vptr     base_vptr
base_attrs    base_attrs

              derived_vptr
              derived_attrs

当您创建一个derived实例时,编译器将调用适当的derived构造函数,其初始化列表以调用base构造函数开头。此时,vtable指针base_vptr被设置为指向基类的虚拟表,因此所有指针都指向base::method。在base构造函数完成后,在派生类中更新base_vptr,并将其设置为指向derived vtable,因此如果在derived中重写了该方法,则实例会指向derived::method。此时,derived_vptr指向在derived中添加的虚拟方法的派生vtable,并且将指向derived::new_method...
只是为了说明一点:vtable不一定存储指向实际方法的指针。在某些情况下,每当调用虚拟方法时都必须执行中间代码,这发生在多重继承时。随着派生类只能与其基类之一对齐(通常是声明的第一个基类),事情变得更加复杂。这就是事情真正变得棘手的地方。向上转型到base2会修改指针,使其指向可以直接解释为base2实例的内存位置,因此类型为base2的指针(内存位置)指向类型为derived的对象的内容将不会与指向同一对象的类型为derived的指针相符。此时,如果调用来自base2的虚拟方法,则系统必须执行一些魔法来重新计算传递给derived::method_from2虚拟方法的隐式this参数的正确位置(它必须指向整个derived对象而不仅仅是base2子对象)。

1

3). 可以这样做:.base* basic = new derived();

basic->BASE::foo(); // 将调用基类方法并在编译时解析。

2). 当类中没有虚拟内容时(如果派生类没有覆盖任何基类函数),是可能的,但如果派生类具有虚拟函数(已覆盖),为了实现运行时多态性,编译器必须通过派生类的vptr初始化基类的vptr,并增加代码。

1). 是的,接口函数原型必须相同,否则基类函数将被派生类函数隐藏(例如,如果参数在派生类函数中不同)。


0

0

第一点: 是的:每个实例都将获得基类声明的成员变量。

第二点: 在这种情况下,您只能访问基类的成员。

第三点: 如果它是虚拟的,您可以调用它,只要它不是纯虚拟的(这意味着它没有任何实现,并且声明方式如下:virtual void foo() = 0;)。


0

您可以在这里查看vtable的实现细节;这是GNU编译器和其他一些主要的Unix编译器使用的方法。特别是“Vtable layout”示例页面非常有教育意义。

正如其他人所指出的,这是一个实现细节,C++标准本身对此没有明确规定。


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