Vtable,计算机术语,中文译名虚函数表,简称虚表。每一个有虚函数的类都有这样一个东西。实际上记录了本类中所有虚函数的函数指针,也就是说是个函数指针数组的起始位置Vtable虚表。比如virtual void TheSecondFun()记录在数组的第二个元素,当一个该类的对象实例调用TheSecondFun时就根据对应关系把第二个函数指针取出来,再去执行该函数,这种行为叫晚绑定,也就是说在运行时才知道调用的函数是什么样子的,而不是在编译阶段就确定的早绑定。

 

多态

Java是一门面向对象的编程语言,面向对象的一-大特色便是多态。 多态的具体体现便是在运行期能够根据对象实例的不同而执行不同的接口方法,换成业界对多态的标准定义便是:允许不同类的对象对同一-消息做出响应,即同一-消息可以 根据发送对象的不同而采用多种不同的行为方式(发送消息就是函数调用)。多态是面向对象编程的特性,而这种特性并不仅仅是喊贼口号就算的,而是必须使用特定的机制或技术去实现。实现多态的技术称为动态绑定(dynamicbinding).是指在执行期间判断所引用对象的实际类型,根据其实际的类型调用其相应的方法。

在Java中,动态绑定也叫“晚绑定”.这是因为在Java中还有一-类绑定 是在编译期间便能确定,所以所谓的晚绑定的概念,是相对于编译期绑定而言的。面向对象编程语言之所以要实现多态这一-特性, 最主要的目的就是为了消除类型之间的耦合关系,通俗地讲就是解桐。从计算机软件一产生,“解糊”便是一切计算机程序所要重点考虑的原则之-一。 其实何止是软件.计算机硬件之间也是以解糊为主要原则的,这类例子举不胜举,例如内存插槽,10接口之类,都是实现解耦的手段。解耦的最大好处在于,一-旦系统发生了变化,能够将变化降低到最小,仅变化新增的部件,而对于已经存在的部件,则尽量保持不变。所以-一个优秀的系统设计师总是想办法设计拥有良好兼容性和扩展性的架构,而面向对象语言的多态性,则是从语言特性上直接实现对象的解桐,这极大地提升了面向对象编程语言构建一套高内聚、 低耦合系统的能力。由于多态通过“动态绑定”的方式得以实现,而绑定通俗一点讲就是让不同的对象对同一个函数进行调用,或者反过来讲就是将同一-个函数 与不同的对象绑定起来,所以多态性得以实现的一个大前提就是,编程语言必须是面向对象的,否则哪来的函数与对象相互绑定一说呢?同时,函数与对象相互绑定,意味着丽数也属于对象的一一部分, 这便具备了封装的特性。因为有了封装,才有了对象。有了对象才叫作面向对象编程。同时,一个函数能够绑定多个不同的对象。意味着多个不同的对象都具有相同的行为,这是继承的含义。因此,面向对象编程语言的三大特性一封装、 继承与多态,其中前两个特性“封装”与“继承”其实就是为了第三个特性“多态”而准备的,或者说“封装”与“继承”成全了“多态”。为“多态”做了嫁衣。

下面是一个简单的动态绑定的示例程序:

清单: Testjava

作用:演示动态绑定

publle abstract class Animal (

abatract vold say(11

publie static void main(Stringll args)l

Animal animal = new Dog();

run (an1mal);

aninal . new Cat0); .

run (animall;

public static void run(Animal animal) 1

animal.say();

1

clasg Dog extends Animal i

Boverride

vold say() 1

System. out .println("I'm a dog");

clas Cat extends Animall

BOverride

vold say() l

System.out,printin("I'm a cat");

}

}

本示例程序中定义了虚类Animal,同时定义了2个子类Dog和Cat,这2个子类都重写了基类中的say0方法。在main0函数中.将animal实例引用分别指向Dog和Cat的实例,并分别调用run(Animal)方法。在本示例中,当在Animal run(Anima)方法中执行animal.say0时,因为在编译期并不知道animal这个引用到底指向哪个实例对象,所以编译期无法进行绑定,必须等到运行期才能确切知道最终调用哪个子类的say0方法.这便是动态绑定,也即晚绑定,这是Java语言以及绝大多数面向对象语言的动态机制最直接的体现。

 

 

C++中的多态与vtable

JVM实现晚绑定的机制基于vtable, 即virtual table,也即處方法表。JVM通过虛方法表在运行期动态确定所调用的目标类的目标方法。在讲解JVM的vtable概念之前,先一起品味C++中虚方法表的实现机制,这两者有很紧密的联系。有如下C++类:

清单: Test.cpp

作用:演示C+H的动态绑定

#include <stdio.h>

include <stdlib.h>

include <string.h>

class CPLUS{

public:

short. x;

public:

void: run()l

thi8->x-2;

1

int. main(int arge, char conat *argv[1)

CPLUS cplusz ,

printr ("sizeof (CPLUS)- 8Iu\n", sizeof (CPLUS));

printt ("&cplus= \n",6cp1us);

printE("G(cplus.x)- p\n",&(cplus.x));

return 0;

这个C++示例很简单,类中包含一个short类型的变量和一个run0方法,在main0函数中打印3个信息: CPLUS 类型寬度、其实例的内存首地址和其变量x的内存地址。编译并执行,输出结果如下:

sizeof (CPLUS)=. 2

scplus- 0x71ff5ef57998

6 (cplus .x)= 0x7fff5ef57998

由于CPLUS类中仅包含1个short类型的变量,因此该类型的数据宽度自然是2。另外注意观察结果中的cplus实例和其变量x的内存地址,两者是相等的。现在将C++类中的run0)方法修改-下,变成虚方法,修改后的程序如下: .

清单: Testcpp

作用:演示C++的动态绑定

Finclude satdio.h>

Winclude <stdlib.h>

include <string.h>

class CPLUS(

public:

short x;

publie:,

virtual void run()(

this->x-2;

main)函数内容不变,因此这里不重复贴出来。编译并运行程序,现在输出变成如下所示:

sizeof (CPLUS)- 16

scplus- 0x78ff5694d990

百(cplus .x)= 0x7f15694998

注意看,现在sizeofCPLUS)的值变成16了,并且cplus实例和其变量x的内存地址也不再相等了。这是咋回事呢?这是因为当C++类中出现虚方法时,表示该方法拥有多态性,此时会根据类型指针所指向的实际对象而在运行期调用不同的方法.这与Java中的多态在语义上是完全一致的。C++为了实现多态,就在C++类实例对象中嵌人虚函数表vtalbe,通过虚两数表来实现运行期的方法分派。C++中所谓虚函数表,其实就是- -个普通的表,表中存储的是方法指针,方法指针会指向目标方法的内存地址。所以虞函数表就是一堆指针的集合而已。对于大部分C++编译器而言,其实现虚函数表的机制都大同小异,都会将虚函数表分配在C++对象实例的起始位置,当C++类中出现虚函数表时,其内存分配就是先分配虚函数表,再分配类中的字段空间。以本示例程序而言,CPLUS 的实例对象cplus的实际内存结构如图8.10所示。

 

 

由于CPLUS类中仅包含一一个虚 函数,因此虚函数表中只有一个指针。注意,在cplus实例的末尾有一-段补白空间, 这是因为C++编译器会对类型做对齐处理,整个C++类实例对象所占的内存空间必须是其中宽度最大的字段所占内存空间的整数倍,而CPLUS类中由于嵌入了虚丽数表,表中元素是指针类型,在64位平台上,1 个指针占8字节内存空间,因此CPLUS类实例对象所占的内存空间就是16字节,这就是上面运行示例程序后输出绪果中的sief(CPLUS)的值变成16的原因所在。同时,字段x被安排在虚函数表之后,因此x的内存地址也不再与ceplus实例对象的内存首地址相等,并且根据上述程序运行结果可见,这两者的内存地址相差8字节,这正好是一个指针的宽度。