虚拟类的每个对象都有一个指向 vtable 的指针吗?

Does every object of virtual class have a pointer to vtable?

提问人:MainID 提问时间:2/18/2009 更新时间:4/20/2016 访问量:13666

问:

虚拟类的每个对象都有一个指向 vtable 的指针吗?

还是只有具有虚函数的基类对象才有它?

vtable 存储在哪里?流程的代码部分还是数据部分?

C++ 继承 vtable

评论

0赞 Anonymous 2/18/2009
重复?stackoverflow.com/questions/99297/......
0赞 curiousguy 4/22/2016
C++中没有“虚拟类”这样的东西。

答:

1赞 Judge Maygarden 2/18/2009 #1

所有虚拟类通常都有一个 vtable,但 C++ 标准不需要它,存储方法取决于编译器。

4赞 Vinay 2/18/2009 #2

Vtable 是每个类的实例,即,如果我有一个具有虚拟方法的类的 10 个对象,则所有 10 个对象之间只有一个 vtable 共享。

在这种情况下,所有 10 个对象都指向同一个 vtable。

评论

0赞 Rndp13 6/2/2015
那么 Vptr 呢,每个对象将有 10 个 vptr 关联,或者像单个 vtable 一样只有一个 vptr?
0赞 Elmar Zander 4/5/2022
@Rndp13 如果 vptr 也与类而不是对象相关联,那么在运行时应该如何解析对重写函数的调用?将 vptr 也放在那里有什么意义,因为我们已经在 vtable 中拥有我们需要的所有信息?
0赞 yesraaj 2/18/2009 #3

每个多态类型的对象都有一个指向 Vtable 的指针。

VTable 的存储位置取决于编译器。

19赞 Michael Burr 2/19/2009 #4

所有具有虚拟方法的类都将具有一个由类的所有对象共享的 vtable。

每个对象实例都有一个指向该 vtable 的指针(这就是 vtable 的查找方式),通常称为 vptr。编译器隐式生成代码以初始化构造函数中的 vptr。

请注意,这些都不是由 C++ 语言强制执行的 - 如果需要,实现可以以其他方式处理虚拟调度。但是,这是我熟悉的每个编译器都使用的实现。Stan Lippman 的著作《Inside the C++ Object Model》很好地描述了它是如何工作的。

评论

2赞 Viet 3/11/2013
+1 您能解释一下为什么虚拟指针是每个对象而不是每个类吗?谢谢。
1赞 Johnson Wong 3/12/2014
@Viet 您可以将 vPtr 视为对象运行时定义的引导程序。只有在设置了 vPtr 之后,对象才能知道其实际类型是什么。在这个概念中,为每个类(静态)制作 vPtr 是没有意义的。换一种方式考虑这个问题,如果一个对象不需要 vPtr,那么它必须在编译时已经知道它的运行时定义,这与它是动态解析的对象相矛盾。
0赞 Scott Langham 2/19/2009 #5

不一定

几乎每个具有虚拟函数的对象都会有一个 v-table 指针。对于具有对象派生自的虚函数的每个类,不需要有一个 v-table 指针。

不过,在某些情况下,充分分析代码的新编译器可能能够消除 v-table。

例如,在一个简单的例子中:如果你只有一个抽象基类的具体实现,编译器知道它可以将虚拟调用更改为常规函数调用,因为每当调用虚拟函数时,它总是解析为完全相同的函数。

此外,如果只有几个不同的具体函数,编译器可以有效地更改调用站点,以便它使用“if”来选择要调用的正确具体函数。

因此,在这种情况下,不需要 v 表,对象最终可能没有 v-table。

评论

0赞 Scott Langham 2/19/2009
嗯。我一直在尝试找到一个可以消除 v-table 指针的编译器。看起来目前没有。但是,编译器和链接器之间的信息共享越来越高,以至于它们正在合并在一起。随着持续发展,这种情况可能会发生。
0赞 jpalecek 2/19/2009
这可能是因为实际上消除 vptr 将意味着严重违反 ABI——这需要确保所讨论的类的任何对象永远不会在模块之外出现——只有 4 个字节的内存,甚至可能实际上没有保存
0赞 jpalecek 2/19/2009
OTOH,只是不通过虚拟调度调用方法只会破坏该特定方法的接口,编译器可以通过发出另一个版本的代码来解决这个问题。它还具有更大的优势,特别是如果功能可以内联
0赞 Johannes Schaub - litb 2/19/2009
是的,看看我下面的例子。省略 V-Table 指针会导致一些令人头疼的问题得到解决。但是,省略 vtables 可能很容易,因此 RTTI 条目将被省略 - gcc 使用 vtable 来引用 RTTI 数据。
4赞 dirkgently 2/19/2009 #6

在家试试这个:

#include <iostream>
struct non_virtual {}; 
struct has_virtual { virtual void nop() {} }; 
struct has_virtual_d : public has_virtual { virtual void nop() {} }; 

int main(int argc, char* argv[])
{
   std::cout << sizeof non_virtual << "\n" 
             << sizeof has_virtual << "\n" 
             << sizeof has_virtual_d << "\n";
}

评论

1赞 dirkgently 2/19/2009
得出必要的结论是为了OP;)left as an assignment
0赞 jalf 2/19/2009
这些数字是典型的,但不是必需的。它没有说明存在多少个 vtable,也没有说明这 4 个字节用于什么。
0赞 Anoop 3/22/2015
在 64 位机器上,它将是 1,8,8。这可能会有点令人困惑,因为空结构的大小是 1 个字节,另外两个分别包含一个指针(如果是 8 位机器,则为 64 个字节)
2赞 Martin York 2/19/2009 #7

VTable 是一个实现细节,语言定义中没有任何内容表明它存在。事实上,我已经阅读了有关实现虚拟函数的替代方法。

但是:所有常见的编译器(即我所知道的编译器)都使用 VTabels。
那么是的。任何具有虚拟方法或派生自具有虚拟方法的类(直接或间接)的类都将具有指向 VTable 的指针的对象。

您提出的所有其他问题将取决于编译器/硬件,这些问题没有真正的答案。

14赞 Johannes Schaub - litb 2/19/2009 #8

就像其他人说的,C++标准不强制要求虚拟方法表,但允许使用虚拟方法表。我已经使用 gcc 和这段代码完成了我的测试,这是最简单的场景之一:

class Base {
public: 
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived1 : public Base {
public:
    virtual void bark() { }
    int dont_do_ebo;
};

class Derived2 : public Base {
public:
    virtual void smile() { }
    int dont_do_ebo;
};

void use(Base* );

int main() {
    Base * b = new Derived1;
    use(b);

    Base * b1 = new Derived2;
    use(b1);
}

添加了 data-members 以防止编译器将基类的大小指定为零(称为 empty-base-class-optimization)。这是 GCC 选择的布局:(使用 -fdump-class-hierarchy 打印)

Vtable for Base
Base::_ZTV4Base: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI4Base)
8     Base::bark

Class Base
   size=8 align=4
   base size=8 base align=4
Base (0xb7b578e8) 0
    vptr=((& Base::_ZTV4Base) + 8u)

Vtable for Derived1
Derived1::_ZTV8Derived1: 3u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived1)
8     Derived1::bark

Class Derived1
   size=12 align=4
   base size=12 base align=4
Derived1 (0xb7ad6400) 0
    vptr=((& Derived1::_ZTV8Derived1) + 8u)
  Base (0xb7b57ac8) 0
      primary-for Derived1 (0xb7ad6400)

Vtable for Derived2
Derived2::_ZTV8Derived2: 4u entries
0     (int (*)(...))0
4     (int (*)(...))(& _ZTI8Derived2)
8     Base::bark
12    Derived2::smile

Class Derived2
   size=12 align=4
   base size=12 base align=4
Derived2 (0xb7ad64c0) 0
    vptr=((& Derived2::_ZTV8Derived2) + 8u)
  Base (0xb7b57c30) 0
      primary-for Derived2 (0xb7ad64c0)

正如你所看到的,每个类都有一个 vtable。前两个条目很特别。第二个指向类的 RTTI 数据。第一个 - 我知道但忘记了。它在更复杂的情况下有一些用处。好吧,如布局所示,如果你有一个类 Derived1 的对象,那么 vptr (v-table-pointer) 将指向类 Derived1 的 v-table,它只有一个条目用于其函数 bark 指向 Derived1 的版本。Derived2 的 vptr 指向 Derived2 的 vtable,它有两个条目。另一个是它添加的新方法,微笑。它重复了 Base::bark 的条目,当然,这将指向 Base 的函数版本,因为它是它最派生的版本。

我还用 -fdump-tree-optimized 转储了 GCC 在完成一些优化后生成的树(构造函数内联,...)。输出使用 GCC 的中端语言,该语言是独立于前端的,缩进到一些类似 C 的块结构中:GIMPL

;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
<bb 2>:
  return;
}

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
<bb 2>:
  return;
}

;; Function int main() (main)
int main() ()
{
  void * D.1757;
  struct Derived2 * D.1734;
  void * D.1756;
  struct Derived1 * D.1693;

<bb 2>:
  D.1756 = operator new (12);
  D.1693 = (struct Derived1 *) D.1756;
  D.1693->D.1671._vptr.Base = &_ZTV8Derived1[2];
  use (&D.1693->D.1671);
  D.1757 = operator new (12);
  D.1734 = (struct Derived2 *) D.1757;
  D.1734->D.1682._vptr.Base = &_ZTV8Derived2[2];
  use (&D.1734->D.1682);
  return 0;    
}

正如我们所看到的,它只是设置了一个指针 - vptr - 它将指向我们之前在创建对象时看到的相应 vtable。我还转储了用于创建 Derived1 的汇编程序代码并调用使用($4 是第一个参数寄存器,$2 是返回值寄存器,$0 是始终 0-register),然后通过工具:)去掉其中的名称c++filt

      # 1st arg: 12byte
    add     $4, $0, 12
      # allocate 12byte
    jal     operator new(unsigned long)    
      # get ptr to first function in the vtable of Derived1
    add     $3, $0, vtable for Derived1+8  
      # store that pointer at offset 0x0 of the object (vptr)
    stw     $3, $2, 0
      # 1st arg is the address of the object
    add     $4, $0, $2
    jal     use(Base*)

如果我们想调用会发生什么?bark

void doit(Base* b) {
    b->bark();
}

GIMPL 代码:

;; Function void doit(Base*) (_Z4doitP4Base)
void doit(Base*) (b)
{
<bb 2>:
  OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
  return;
}

OBJ_TYPE_REF是一个 GIMPL 结构,它被很好地打印出来(它记录在 gcc SVN 源代码中)gcc/tree.def

OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>)

它的含义是:在对象上使用表达式,并将前端 (c++) 特定值(它是 vtable 中的索引)存储。最后,它作为“this”参数传递。我们是否可以调用出现在 vtable 中第二个索引处的函数(注意,我们不知道哪个 vtable 属于哪种类型!),GIMPL 将如下所示:*b->_vptr.Baseb0b

OBJ_TYPE_REF(*(b->_vptr.Base + 4);b->1) (b) [tail call];

当然,这里再次是汇编代码(堆栈帧的东西被切断了):

  # load vptr into register $2 
  # (remember $4 is the address of the object, 
  #  doit's first arg)
ldw     $2, $4, 0
  # load whatever is stored there into register $2
ldw     $2, $2, 0
  # jump to that address. note that "this" is passed by $4
jalr    $2

请记住,vptr 正好指向第一个函数。(在该条目之前,存储了 RTTI 插槽)。因此,该插槽中出现的任何东西都被称为。它还将调用标记为尾调用,因为它发生在我们函数中的最后一个语句中。doit

2赞 MSN 2/19/2009 #9

要回答有关哪些对象(从现在开始的实例)具有 vtable 以及在哪里的问题,考虑何时需要 vtable 指针会很有帮助。

对于任何继承层次结构,对于该层次结构中特定类定义的每组虚拟函数,都需要一个 vtable。换言之,给定以下几点:

class A { virtual void f(); int a; };
class B: public A { virtual void f(); virtual void g(); int b; };
class C: public B { virtual void f(); virtual void g(); virtual void h(); int c; };
class D: public A { virtual void f(); int d; };
class E: public B { virtual void f(); int e; };

因此,您需要五个 vtable:A、B、C、D 和 E 都需要自己的 vtable。

接下来,您需要知道给定指向特定类的指针或引用要使用哪个 vtable。例如,给定一个指向 A 的指针,您需要对 A 的布局有足够的了解,以便您可以获得一个 vtable,告诉您在哪里调度 A::f()。给定指向 B 的指针,您需要充分了解 B 的布局才能调度 B::f() 和 B::g()。依此类推。

一种可能的实现可以将 vtable 指针作为任何类的第一个成员。这意味着 A 实例的布局为:

A's vtable;
int a;

B 的实例是:

A's vtable;
int a;
B's vtable;
int b;

您可以从此布局中生成正确的虚拟调度代码。

您还可以通过组合具有相同布局的 vtables 的 vtable 指针来优化布局,或者如果一个指针是另一个 vtable 的子集。因此,在上面的示例中,您还可以将 B 布局为:

B's vtable;
int a;
int b;

因为 B 的 vtable 是 A 的超集。B 的 vtable 有 A::f 和 B::g 的条目,A 的 vtable 有 A::f 的条目。

为了完整起见,以下是我们目前看到的所有 vtables 的布局方式:

A's vtable: A::f
B's vtable: A::f, B::g
C's vtable: A::f, B::g, C::h
D's vtable: A::f
E's vtable: A::f, B::g

实际条目为:

A's vtable: A::f
B's vtable: B::f, B::g
C's vtable: C::f, C::g, C::h
D's vtable: D::f
E's vtable: E::f, B::g

对于多重继承,执行相同的分析:

class A { virtual void f(); int a; };
class B { virtual void g(); int b; };
class C: public A, public B { virtual void f(); virtual void g(); int c; };

生成的布局将是:

A: 
A's vtable;
int a;

B:
B's vtable;
int b;

C:
C's A vtable;
int a;
C's B vtable;
int b;
int c;

您需要一个指向与 A 兼容的 vtable 的指针和一个指向与 B 兼容的 vtable 的指针,因为对 C 的引用可以转换为对 A 或 B 的引用,并且您需要将虚函数分派给 C。

由此可以看出,特定类的 vtable 指针数至少是它派生的根类数(直接派生或由于超类)。根类是具有 vtable 的类,该 vtable 不继承自同样具有 vtable 的类。

虚拟继承在混合中抛出另一个间接因素,但您可以使用相同的指标来确定 vtable 指针的数量。

评论

0赞 Validus Oculus 4/23/2016
当您投反对票时,请指出答案中的错误之处。否则,我们没有办法改进内容!谢谢。