Virtual method table(虚拟方法表):

    一个虚拟方法表(VMT)/虚拟功能表/虚拟呼叫表/分发表是使用在编程语言中支持动态分发(或运行时方法绑定)的一种机制。

    无论何时,一个类定义了一个虚拟方法,大多数编译器向类指定一个指向虚拟方法表(VMT或Vtable)的(虚拟)函数的指针数组添加一个隐藏的成员变量。这些指针在运行时用于适当的函数调用,因为在编译时它可能还不知道是否要调用基函数或者是从继承基类的类实现的派生类。

    假设一个程序在继承层次结构中包含几个类:一个父类: Cat,两个子类: HouseCat, Lion。Cat类定义了一个名叫speak的虚拟函数(方法),因此他的子类可能提供一个适当的实现。(例如:meow或者roar)。

    当程序调用一个Cat引用(可以指代实例Cat,或者HouseCat,Lion实例)上调用speak函数时,这个代码必须能够确定调用应该被调用到的函数的实现。这取决于对象的实际类,而不是它声明的类Cat。这个类通常不能静态地(即,在编译时)确定类,所以编译器也不能决定哪个函数在运行时被调用了。这个调用必须动态地(即,在运行时)被分发到正确的函数。

    实现这种动态分发有很多不同的方法,但是虚拟方法表解决方案在C++及其相关语言(如D和C#)中尤为常见。将对象的编程接口从实现分离出来的语言,像Visual Basic和 Delphi, 也可以使用虚拟表实现。因为它允许对象使用不同的实现,只需要使用一组不同的方法指针。

内容

1. Implementation(实现)

2. Example(例子)

3. Multiple inheritance and thunks(多重继承和指针修复)

4. Invocation(调用)

5. Efficiency(效率)

6. Comparison with alternatives(比较和替代)

7. See also(参见)

8. Notes(笔记)

9. References(参考)

1. Implementation(实现)

    一个对象的调度表将包含对象的动态绑定方法的地址,方法调用是通过从对象的调度表提取方法地址来执行的。调度表对于属于同一类的所有对象是相同的,因此通常在它们之间共享。属于类型兼容类的对象(例如继承层次结构中的兄弟)将有相同布局的调度表:给定方法的地址将对所有兼容类将以相同的偏量出现。因此,从给定的调度表偏移得到方法的地址将获得对应于对象的实际类的方法[1]。

    C++规范并没有要求必须如何实现动态调度,但是编译器在相同基础模型上通常使用较小的变化。

    典型地,编译器为每个类创建一个单独的虚拟表。当一个对象被创建时,一个指向这个虚拟表的指针,被称为虚拟表指针,vpointer或VPTR,作为该对象的隐藏成员添加。因此,编译器必须在每个类的构造函数生成“隐藏”代码,去初始化一个新对象的虚拟表指针到这个类的虚拟表指针。

    很多编译器将虚拟表指针作为对象的最后一个成员,而另外的编译器将虚拟指针表作为对象的第一的成员。便携式源代码工作方式[2]。例如,g++以前将虚拟表指针放在对象的末尾[3]。


2. Example(例子)

    思考下面C++语法的类声明:

class B1 {
public:
  void f0() {}
  virtual void f1() {}
  int int_in_b1;
};

class B2 {
public:
  virtual void f2() {}
  int int_in_b2;
};


    使用派生以下类:

class D : public B1, public B2 {
public:
  void d() {}
  void f2() {}  // override B2::f2()
  int int_in_d;
};


    下面是C++代码片段:

B2 *b2 = new B2();
D  *d  = new D();


    g++ 3.4.6 从GCC为对象b2产生下面32位存储布局[nb 1]:

b2:

  +0: pointer to virtual method table of B2

  +4: value of int_in_b2

virtual method table of B2:

  +0: B2::f2()

    下面是对象d的存储布局:

d:

  +0: pointer to virtual method table of D (for B1)

  +4: value of int_in_b1

  +8: pointer to virtual method table of D (for B2)

 +12: value of int_in_b2

 +16: value of int_in_d

Total size: 20 Bytes.

virtual method table of D (for B1):

  +0: B1::f1()  // B1::f1() is not overridden

virtual method table of D (for B2):

  +0: D::f2()   // B2::f2() is overridden by D::f2()

    注意这些函数在声明(像f0()和d)时并不携带关键字virtual,一般不会出现在虚拟表中。由默认构造函数构成的是特例。

    在类D中重写方法f2()是通过复制虚拟方法表B2并将指针B2::f2()替换成D::f2()实现的。


3. Multiple inheritance and thunks(多重继承和指针修复):

    g++编译器实现了在类D中使用两个虚拟方法表对类B1和B2的多重继承,它们都是类D的基类。(实现多重继承还有另一种方法,但这是最常用的)。这使指针修复成为必要。也叫thunks

    思考下面C++ 代码:

D  *d  = new D();
B1 *b1 = d;
B2 *b2 = d;


    当执行这段代码后,d和b1指向同一存储位置。b2将指向d+8的位置(8位超出了d的存储位置)。因此,b2指向d中的“看起来像”B2的实例的区域,即具有与B2的实例相同的存储器布局。


4. Invocation(调用):

    d->f1()调用的处理方式是通过取消d的D::B1的虚拟指针引用,查找虚拟方法表中f1项,然后取消这段调用代码的引用。

    在单项继承的例子中(或者是一种语言中的单项继承),如果虚拟指针始终在d的第一元素(与许多编译器一样)这将减少到以下伪C++.

(*((*d)[0]))(d)

    *d是指,虚拟方法表中D和[0]指向虚拟方法表中的第一个方法。参数d成为了指向这个对象的指针。

    在更一般的情况下,调用 B1::f1() 或 D::f2()更复杂些:

(*(*(d[+0])[0]))(d)  

(*(*(d[+8])[0]))(d+8)

    调用d->f1()是通过将B1指针作为一个参数。调用d->f2()是通过将B2指针作为一个参数。这第二个调用需要一个修复(fixup)来产生正确的指针。调用B2::f2是不可能的,因为在D的实现中它已经被重写了。B2::f2的位置已经不在D的虚拟表中。通过对比,调用d->f0()更简单:

(*B1::f0)(d)


5. Efficiency(效率):

    一个虚拟调用要求至少一个额外的索引取消引用,有时候一个“fixup”的增加,与一个非虚拟调用功能相比,这只是简单的跳转到编译指针。因此调用虚拟方法本质上比调用非虚拟方法慢。一个在1996年做的实现表明,大约6~13%的执行时间是花在简单的调度到正确的方法。虽然开销可以高达50%[4]。虚拟方法的花费在现代的CPU架构上可能不是那么高,由于更大的缓存和更好的分支预测。

    进一步说,在JIT编译没有使用的环境中,虚拟方法调用通常不能内联。在某些情况下,编译器可能会执行称为半虚拟化的进程。例如,查找和间接调用被每个内联体的条件执行替换,但是这种优化并不常见。

    为了避免这些开销,编译器通常在编译时可以解决的调用时避免使用虚拟表。

    因此,以上对f1的调用可能不需要一个虚拟表查询,因为编译器可能可以告诉d在这个点上只能持有D,并且D不重写f1。或者编译器(或者优化器)或许可以检测到在程序的任何地方没有B1的子类覆盖f1。调用B1::f1 或B2::f2可能不会要求一个虚拟表查询,因为明确地指定了实现。(虽然它仍需要‘this’指针的fixup)


6. Comparison with alternatives(比较和替代):

    虚拟表与实现动态调度通常是一个很好的性能交易,但是仍然有适用条件,例如二进制树分发,具有较高的性能但是成本不一[5]。

    然而,虚拟表只允许在特殊参数‘this’上进行单次调度。相比之下,多次调度(例如在CLOS或Dylan)可以在调度时考虑所有参数的类型。虚拟表只有在调度被约束到已知的一组方法时才起作用。所以它们可以放在一个编译时建立的简单数组,与鸭式打字语言(如Smalltalk,Python或JavaScript)相反。

    提供这些功能中的一个或两个的语言通常通过在hash表查找字符串或其他一些等效的方法进行调度。有各种不同的技术可以使他更快(例如,实习/令牌化方法名称,高速缓存查找,即时编译)。


7. See also(参见):

    虚拟方法

    虚拟继承

    分支表


8. Notes(笔记)

    G ++的-fdump-class-hierarchy参数可用于转储虚拟方法表以进行手动检查。 对于AIX VisualAge XlC编译器,请使用-qdump_class_hierarchy转储类层次结构和虚拟功能表布局。


9. References(参考)

    Margaret A. Ellis and Bjarne Stroustrup (1990) The Annotated C++ Reference Manual. Reading, MA: Addison-Wesley. (ISBN 0-201-51459-1)

    [1]. Ellis & Stroustrup 1990, pp. 227–232

    [2]. Danny Kalev. "C++ Reference Guide: The Object Model II". 2003. Heading "Inheritance and Polymorphism" and "Multiple Inheritance".

    [3]. C++ ABI Closed Issues at the Wayback Machine (archived 25 July 2011)

    [4]. Driesen, Karel and Hölzle, Urs, "The Direct Cost of Virtual Function Calls in C++", OOPSLA 1996

    [5]. Zendra, Olivier and Driesen, Karel, "Stress-testing Control Structures for Dynamic Dispatch in Java", Pp. 105–118, Proceedings of the USENIX 2nd Java Virtual Machine Research and Technology Symposium, 2002 (JVM '02)

原文来自:https://en.wikipedia.org/wiki/Virtual_method_table#cite_note-4



总结:每个对象的指针的第一个地址指向一张表(虚拟方法表),这个表里会有各方法实现的地址

具体:

虚拟方法表:

前提:在new一个对象时会先检查这个对象有没有父类,如果有父类就会在这个对象的第一个指针地址上创建一张虚拟方法表。

每一个对象地址会有一个指针,这个指针指向一张表(即虚拟方法表),这个表里会有一个叫call的名字,这个名字会有一个地址,指向这个名字的方法实现。

但是如果子类的方法不是继承自父类,而是子类自己特有的就不会在虚拟方法表里存在。

如果子类继承了父类但是没有重写父类的这个方法,这个会记录在虚拟方法表里,这个名字指向的地址就是父类这个方法的地址。

public class Callable{
  public void call() {
  System.out.print("say hello");
  }

  public void back(){
  System.out.println("say bye");
  }
}
public class void CallExecutor extends Callable{

  @Override
  public void call() {
  System.out.println("say hello world");
  }

  public void doOther() {
  System.out.printlln("do any other thing...");
  }
}
public class Achieve{
  public void getCall(Callable callable){
  callable.call();
  }
}
public void main(String[] args) {
  Achieve achieve = new Achieve();
  Callable object = new CallExecutor();
  achieve.getCall(object);
}


在这里,new CallExecutor这个对象时生成一个虚拟方法表

vtable:

VPTR

--------------------------------

call -> 0x00000010-----

public static CallExecutor(CallExecutor this)
{
  public void call()
  {
   System.out.println("say hello world")
  }
}


back -> 0x00000050-----

这个地址就是父类back方法的地址(同一个方法的实现在内存中只有一个方法块)

public static CallExecutor(CallExecutor this)
{
  public void back(){                    
    System.out.println("say bye"); 
  }
}



--------------------------------