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

时间:2009-02-18 15:47:44

标签: c++ inheritance vtable

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

或者只有具有虚函数的基类对象具有它?

vtable存放在哪里?流程的代码部分或数据部分?

9 个答案:

答案 0 :(得分:15)

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

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

请注意,这些都不是C ++语言强制要求的 - 如果需要,实现可以通过其他方式处理虚拟调度。但是,这是我熟悉的每个编译器都使用的实现。 Stan Lippman的书“Inside the C ++ Object Model”描述了它的工作原理。

答案 1 :(得分:12)

就像其他人所说的那样,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);
}

添加了数据成员,以防止编译器给基类的大小为零(它被称为空基类优化)。这是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的中端语言GIMPL,它是前端独立的,缩进为一些类似C的块结构:

;; 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-寄存器)之后用{{1来解析它中的名称工具:)

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

GIMPL代码:

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

;; Function void doit(Base*) (_Z4doitP4Base) void doit(Base*) (b) { <bb 2>: OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call]; return; } 是一个GIMPL构造,它被打印出来(它在gcc SVN源代码的OBJ_TYPE_REF中有记录)

gcc/tree.def

这意味着:在对象OBJ_TYPE_REF(<first arg>; <second arg> -> <third arg>) 上使用表达式*b->_vptr.Base,并存储前端(c ++)特定值b(它是vtable中的索引)。最后,它将0作为“this”参数传递。我们会调用一个出现在vtable中第二个索引的函数(注意,我们不知道哪个类型的vtable!),GIMPL看起来像这样:

b

当然,这里再次汇编代码(堆栈框架内容被切断):

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

请记住,vptr完全指向第一个函数。 (在该条目之前存储了RTTI插槽)。因此,无论在该位置出现什么都被调用。它还将调用标记为尾调用,因为它是我们 # 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 函数中的最后一个语句。

答案 2 :(得分:4)

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

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

答案 3 :(得分:4)

在家尝试:

#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";
}

答案 4 :(得分:2)

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

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

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

答案 5 :(得分:1)

所有虚拟类通常都有一个vtable,但C ++标准并不要求它,并且存储方法依赖于编译器。

答案 6 :(得分:1)

要回答关于哪些对象(从现在开始的实例)有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;

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

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

B's vtable;
int a;
int b;

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

为了完整起见,这就是你如何布置我们目前看到的所有vtable:

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指针的数量。

答案 7 :(得分:0)

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

存储的VTable依赖于编译器。

答案 8 :(得分:0)

不一定

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

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

例如,在一个简单的情况下:如果您只有一个抽象基类的具体实现,编译器知道它可以将虚拟调用更改为常规函数调用,因为无论何时调用虚函数,它都将始终解析完全相同的功能。

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

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

相关问题