1、什么是虚方法?
考虑Animal* pa = new Dog(); pa表面类型是Animal,实际类型是Dog。可以理解为,pa说,我指向Animal,说法是对的。但是不具体,实际上,pa指向Dog。pa->Say()是虚方法,在编译期,编译器只知道pa 的表面类型,不知道该调用Animal 的Say方法还是Dog 的Say方法,所以才叫做“虚方法”。只有在运行期,才根据pa 的真实类型,确定调用哪个方法。这就是虚方法。
2、为什么需要虚方法?它解决了什么问题?
简单说,就是为了面向接口编程,库的提供者暴露接口,隐藏实现。库的使用者不需要知道内部的实现细节。
3、它是如何解决的?
虚方法也就是运行时多态。要实现运行时多态,怎么办?思考,运行时多态的本质特征是,相同的方法名,却导致调用不同的方法。因此,这里需要加一个间接层,内部封装,暴露接口。现在简单讲一下,目前的C++是如何实现多态的?
考虑,Animal类有一个虚方法表(可认为是一个数组,元素是方法指针),里面有100个虚方法,Dog类会整体拷贝Animal类的虚方法表,对于Dog类重写的方法,在虚方法表的对应位置偷梁换柱,换上重写后的方法,对于Dog类新增的虚方法,在虚方法表中后面加上。
类的对象内存中只有,实例字段和vptr两块内容,其中vptr指向类的虚方法表。假设Animal对象的内存布局为:age,name,vptr,Animal对象的vptr指向Animal类的虚方法表。Dog对象的内存布局为age,name,vptr,color,Dog对象的vptr指向Dog类的虚方法表。
考虑pa->Say()虚方法的调用,编译器知道Say是虚方法,通过vptr间接调用,在运行期才能确定下来。pa实际指向Dog对象,编译器把pa指向内容当作Animal对象来解释,这有没有问题呢?
在上面对象的内存布局看到,Animal对象和Dog对象在前面部分是一样的,Dog对象追加了一些内存。把pa指向的内容当作Animal对象解释,取第三个字段vptr,也就是Dog 对象的vptr,我们知道Do对象的vptr,指向Dog类的虚方法表。也就是说,会调用Dog类重写的虚方法。
4、用法
在父类中,为了表明是虚方法,在方法前加上virtual,子类重写续方法。子类中的方法不需要在说明,是virtual,会自动生成为virtual,但是,为了直观,建议子类中也使用virtual。
5、注意事项
a、没有虚方法的类,这个类也没有对应的虚方法表,也可能是对应的虚方法表为空,类的对象也就没有vptr。因此,不要随便添加无用的虚方法,否则,会导致对象变大,要多一个vptr字段。
b、只有表面类型和真实类型不同的情况下,才存在多态。也就是说,只有指针或者引用才存在多态。为啥?对于类对象而言,真实类型,就是声明的表面类型。子类对象赋值给父类对象,会出现对象切割,也就是把子类的部分切割掉,父类的部分整体拷贝。
c、构造方法和析构方法中,不要调用虚方法,因为达不到预期的效果。为啥?子类构造方法是在父类构造方法完成的基础上进行的。从这个角度讲,构造析构可以类比穿衣脱衣。穿衣时,先穿内衣,再穿外套。脱衣时,先脱外套,再脱内衣。
考虑,在父类构造方法内调用虚方法,期望调用子类重写的方法。这个是不行的,为啥?在父类构造方法中,当前对象不是一个完整的子类对象,还没有子类部分,也就是说,当前情况下的真实类型就是父类对象,当然不可能有多态效果。
那么析构方法呢?在父类的析构方法中调用虚方法,期望调用子类重写的虚方法。这个也是不行的,为啥?从穿衣脱衣的例子中,我们知道在父类的析构方法中,子类部分已经销毁了。
6、C++虚方法实现的局限性
通过上面的分析,知道C++虚方法的实现,是子类和父类都保存一个虚方法表,这就导致同样的值,存储多次,耗费多余的内存,特别是极端的情况下,顶层父类有许多虚方法,下面有衍生出一大堆的子类,每一个类都有一个很大的虚方法表。同样的值,存储多次是很愚蠢的。
C++虚方法实现的优点也是很明显的,由于子类和父类的虚方法表,在位置上一一对应,要么是同样的方法指针,要么二者是重写关系,当然也有可能是子类新增加了一些虚方法,对应的父类位置为空。这样,就使得方法的查询效率很高,根据下标直接定位。