当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《虚拟类的每个对象都有一个指向vtable的指针吗?》经验,为你挑选了2个好方法。

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

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

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



1> Michael Burr..:

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

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

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


+1你能解释为什么虚拟指针是每个对象,而不是每个类?谢谢.

2> Johannes Sch..:

像其他人说的那样,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的函数版本,因为它是它的最衍生版本.

在完成一些优化(构造函数内联,......)之后,我还抛弃了由GCC生成的树,并使用-fdump-tree优化.输出使用GCC的中端语言GIMPL,它是前端独立的,缩进为一些类似C的块结构:

;; Function virtual void Base::bark() (_ZN4Base4barkEv)
virtual void Base::bark() (this)
{
:
  return;
}

;; Function virtual void Derived1::bark() (_ZN8Derived14barkEv)
virtual void Derived1::bark() (this)
{
:
  return;
}

;; Function virtual void Derived2::smile() (_ZN8Derived25smileEv)
virtual void Derived2::smile() (this)
{
:
  return;
}

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

:
  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-寄存器)在用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)
{
:
  OBJ_TYPE_REF(*b->_vptr.Base;b->0) (b) [tail call];
  return;
}

OBJ_TYPE_REF是一个很好的打印的GIMPL构造(它gcc/tree.def在gcc SVN源代码中记录)

OBJ_TYPE_REF(;  -> )

它的含义是:*b->_vptr.Base在对象上使用表达式b,并存储前端(c ++)特定值0(它是vtable中的索引).最后,它b作为"这个"论点传递.我们会调用一个出现在vtable中第二个索引的函数(注意,我们不知道哪个类型的vtable!),GIMPL看起来像这样:

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函数中的最后一个语句.

推荐阅读
落单鸟人
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有