Skip to content

Latest commit

 

History

History
119 lines (66 loc) · 8.33 KB

多态.md

File metadata and controls

119 lines (66 loc) · 8.33 KB

什么是多态

多态是OOP中的核心思想,当我们使用基类的引用调用基类中定义的一个函数时,并不知道该函数真正作用的对象是什么类型。它可能是基类的对象也可能是派生类的对象,这在编译时期是判断不了的,只有在运行时期,才能根据引用或指针所绑定的对象的真实类型来决定。

C++中的多态实现

在c++中每个类都会有一个虚函数表(每个类仅有一个),所有的对象共用这张表。c++中的函数多态就是通过虚函数来实现的。

要理解具体的实现,首先得要理解c++中的一些概念:

虚函数

在函数前加上virtual,如果一个函数 在基类中被声明为virtual,那么在它的所有派生类中它都是virtual的,而在派生类中对virtual函数的重定义称为重写。也就是说,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明为虚函数。注意,子类可以不用重写父类中的虚函数。

纯虚函数

不能被用来声明对象,是抽象类。带有纯虚函数的类就是抽象类。子类必须重写父类中的虚函数,除非子类也是抽象类。

绑定

把函数体和函数调用相联系称为绑定。又分为早绑定和晚绑定

早绑定:绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定

晚绑定(动态绑定、运行时绑定):早绑定不能实现多态,这时候需要晚绑定,根据对象的类型,发生在运行时。当使用基类的引用调用一个虚函数时将发生动态绑定,所以晚绑定只对虚函数起作用。

如何实现晚绑定

虚表

关键字virtual告诉编译器它不应当执行早绑定,相反,它应当自动安装对于实现晚绑定必须的所有机制。为了达到这个目的,编译器对每个包含虚函数/纯虚函数的类创建一个虚表(VTABLE),在虚表中,编译器会放置特定类的虚函数地址。每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个唯一的虚表VTABLE,这个表中放置了在这个类中或它的基类中所有已声明为virtual函数的地址

虚表指针

那么如何找到这个表呢?答案是通过虚表指针

在每个带有虚函数的类中,编译器会放置一个VPTR,指向这个对象的VTABLE。当通过基类指针做虚函数调用时(也就是做多态调用时),编译器静态地插入能取得这个VPTR并在VTABLE表中查找函数地址的代码,这样就能调用正确的函数并引起晚捆绑的发生

如何取出对象的VPTR?

对于所有的基类对象或右基类派生的对象,它们的VPTR都在对象的相同位置(常常在对象的开头),所以编译器能够取出这个对象的VPTR。VPTR指向VTABLE的起始地址。所有的VTABLE都具有相同的顺序(按照声明的顺序),即虚方法在类中的顺序是确定的,这样就可以通过VPTR和虚方法在虚表中的地址偏移找到它

要注意:

1、如果子类继承了父类,但是并没有重写父类中的虚函数,那么在虚表中父类的虚函数在子类的虚函数前面

2、如果子类继承父类并且重写父类中的虚函数,那么子类中重写父类的函数会放到虚表中原来父类虚函数的位置,没有被重新的函数依旧。

3、如果是多继承,那么每个父类都有自己的虚表,子类的成员函数被放到第一个父类(按照声明顺序)的表中。

假设现在子类调用了父类的虚函数N并且没有重写,那么就能根据虚表指针和该虚函数在虚表中的地址来确定到底调用的是哪个函数。

如果子类重写了该方法:

虚表分发

就是上面这两张图的过程,通过虚表内存地址拿到虚表记录,然后通过函数名+参数信息到虚表中去找。因为是从前往后找,所以如果子类重写了父类方法,那么就会调用子类的方法。并且从上面的图可以看出,虚表其实就是数组实现的,前面的数字就是索引

Java中的多态实现

jvm是用c++写的,所以java中多态实现跟c++的多态实现是相似的,比如虚表也是数组实现的,但是还是有不一样的地方。

虚表在哪

因为java中的类,在jvm中对应的c++对象是klass模型,java中的对象,在jvm中对应的c++对象是oop模型。前面我们知道,c++中的虚表是由VPTR指向的,而这个VPTR在类对象的头部。所以jvm中的虚表在klass模型的头部,即java类对象的头部

我们看下klass.hpp源码:

//  Klass layout:
//    [C++ vtbl ptr  ] (contained in Metadata)
//    [layout_helper ]
//    [super_check_offset   ] for fast subtype checks
//    [name          ]
//    [secondary_super_cache] for fast subtype checks
//    [secondary_supers     ] array of 2ndary supertypes
//    [primary_supers 0]
//    [primary_supers 1]
//    [primary_supers 2]
//    ...

可以看到,klass内存布局的第一个就是虚表指针。

jvm中的虚表存的是什么

我们知道java中是没有虚函数这个概念的,c++中的虚函数其实对应的就是java中的普通函数,而纯虚函数对应的就是java中的抽象函数。所以在java中,随便定义一个类,它是有虚表的,关键是这个虚表中存放的是哪些方法地址。而只有被public、protect类型的,且不被static、final修饰的方法才能被多态调用,所以才会进入虚表。

比如Object类,它里面满足这些条件的方法都会进入虚表,我们可以通过HSDB工具查看:

jvm如何实现虚表分发

在多态中,父类引用指向子类对象,然后进行方法调用,所以有必要了解jvm中的方法调用。在c++中有直接调用和间接调用,jvm则抽象成了4个指令来完成:

1、invokevirtual:这个指令用于调用public、protected修饰,且不被static、final修饰的方法。跟多态机制有关。

2、invokeinterface:跟invokevirtual差不多。区别是多态调用时,如果父类引用是对象,就用invokevirtual。如果父类引用是接口,就用这个。

3、invokespecial:只用于调用私有方法,构造方法。跟多态机制无关。

4、invokestatic:只用于调用静态方法。与多态机制无关。

ps:现在又新增了一条指令invokedynamic,该指令用于调用动态方法

​ 这里主要说下第2个指令,因为它和其他指令不一样,其他指令的后面就是2个操作数(常量池索引),拿着操作数去常量池中就可以找到类信息、方法信息,但是invokeinterface后面的操作数占了4个字节。第一个操作数和第二个操作数加起来就是常量池的索引,对应常量池项CONSTANT_InterfaceMethodref,它包含了接口方法的名称和描述符,以及对该方法所在接口的符号引用。第3个操作数是方法的参数个数,需要注意非静态方法就算没有参数,也会默认有一个,就是this指针,long和double类型的参数占用2个数量单位,其实这些信息完全可以从方法的描述符中获取到,之所以有这个参数完全是历史原因。第4个操作数用于维持向后兼容性

​ 所以在调用的时候,会通过这个this指针从操作数栈拿到真正的对象,然后通过对象头中的类型指针拿到该类对应的C++类对象,即klass模型,然后通过函数名+参数信息及返回值信息根据VPTR去虚表中找,如果子类重写了父类方法,那么就会调用子类方法,和上面c++中的虚表分发是一样的逻辑,这就是jvm虚表分发的底层实现

总结

c++的多态是通过对存在虚函数的类创建一个虚表,然后通过虚表分发实现。

而jvm中,因为java没有虚函数的概念,所以jvm中的虚表中存的方法也有区别,并且jvm将c++中的方法调用抽象成了4个调用指令,而与多态相关的就只有invokevirtual**invokeinterface**指令,执行这些指令,会将this传递过来,通过它就可以往后找到虚表中的方法地址,从而完成运行时的方法调用。