C++是一门面向对象的程序设计语言,其实这句话不严谨。严格来讲C++不仅支持面向对象的编程风格,还支持基于对象、泛型、模板还有函数式的编程风格。通常来讲面向对象是C++中比较常用的一面。
1. 什么是多态?
面向对象的三大基本特征,封装、继承、多态。多态(polymorphism)指为不同数据类型的实体提供统一的接口,这样做的好处就是我们可以在不修改原有程序的前提下删除或者增加一个新的派生类。有人把多态进行广义的解释,认为多态分两种,一种是静态多态,另一种则是我们要讲的动态多态。
静态多态:在编译期间就已经确定的,比如函数重载,函数模板。
动态多态:也称为运行时的多态,是指程序运行时根据所引用对象的实际类型调用相应的方法。通常所说的动态绑定也是这个意思。下文中指的多态,默认是动态多态。
2. 多态的实现
简单来说,virtual关键字修饰基类的成员函数,派生类中重写此函数。程序中,基类指针指向派生类对象,运行时调用了基类开发的那个函数/接口/方法,但实际会调用实例化的派生类对象重写的那个函数。那么virtual关键字背后是怎样的机制呢?
C++程序在编译时,遇到virtual关键字,会在生成一个对应此类的虚函数表,把虚函数存放到虚函数表中,类中只存放一个虚函数表指针,当对象调用此函数时,可以通过虚函数表指针进行访问。(tip:所以虚表指针的初始化很重要,如果没有初始化就别去调用了,例如在构造函数中调用虚函数,派生类在构造过程中先基类构造,基类构造函数中调用虚函数,这个虚函数是不会调用派生类的实现的,因为这个时候派生类的虚表指针还没初始化完,所以不要在构造函数中调用虚函数) 派生类在生成虚函数表的时候,会参考父类虚函数表,如果子类中有的虚函数,不管是新增还是重写父类虚函数,都优先写入自己的虚函数表,其他虚函数和普通成员函数一样,都会根据继承权限从基类继承下来。
另外,如果虚函数后面加=0,说明此虚函数为纯虚函数,不需要实现,当然这个类也不能实例化。它设计的目的主要为了单纯的声明一个接口。
这里附上侯捷老师的PPT,如下图
3. 多态的代价
多态是通过晚绑定来实现的,也就是对象实例化的时候,虚函数表指针初始化执行虚函数表之后,可以通过它来访问虚函数。那么虚函数表指针是必须的,这块内存必须要占用。对象和虚表指针是一一对应的,有N个对象,就会有N个虚表指针。空间占用是多态的最直接代价。
另一个代价就是函数寻址变慢,非虚函数可以直接访问,而虚函数则要通过虚表指针,多了一次寻址。
4. 总结
既然3中讲了多态在空间和时间上都有代价,那么C++为什么还继续使用多态呢?
多态使我们在不修改原有程序的前提下删除或者增加一个新的派生类,方便了我们的开发,而且更符合我们的思维习惯。也就是说它提高了代码的可维护性和扩展性,而这对于性能要求不那么苛刻的程序而言,牺牲一点效率往往是可取的。
其他内容,可参考