动态绑定/多态

DinS          Written on 2017/10/31

动态绑定是OOP的灵魂。因为有了动态绑定,程序呈现出了一种“智能”。
动态绑定(dynamic binding)还有另一个类似的叫法:多态(polymorphism)。这两个术语指的是同一件事:引用或指针的静态类型与动态类型不同,但是二者侧重点不同。
动态绑定强调的是绑定的过程,即我们虽然在代码中使用基类的接口,但是实际运行时会根据指针或引用绑定的真实类型调用对应派生类的接口。
多态强调的是类的状态,即虽然使用基类编程,但是实际上这个基类却在运行时可以变化为不同的派生类。

为了实现动态绑定,c++提供了关键概念:虚函数。
当一个类的接口被声明为虚函数后,当发生向上类型转换时程序会自动找到对应的派生类中的接口进行调用。
虚函数背后的原理在这里不赘述了,让我们来看看实际的代码,不要停留在“虚”上。

声明虚函数很简单,就在在最前面加上关键字virtual即可。
永远将基类的析构函数声明为虚函数。这是因为动态绑定也适用于析构函数,如果真实的类型是派生类,但是基类的析构函数不是虚的,那么对象会执行基类的析构函数。造成的结果是派生类的基类部分被销毁了,但是新派生的部分没有被销毁,也就是内存泄露了。
构造函数无法声明为虚。

刚才的Fruit基类的public里增加一个接口,然后声明为virtual即可。

基类虚函数实现:

按照常理的总价=单价X重量。

然后看看派生类:

跟刚才唯一的区别是在接口最后加了一个override。
派生类中与基类对应的接口,函数签名必须一致,这样编译器才能找到对应的接口,然后覆盖对应的函数。
派生类中不必再声明virtual,只要基类中声明了virtual派生类中的同样接口自动是虚函数。
override关键字是c++11新标准,可以不加这个也能够实现虚函数覆盖。但是推荐加上关键字override。这是因为加上后编译器会在编译时进行安全检查,确保派生类确实覆盖了基类虚函数。一个常常发生的情况是程序员少打了些东西结果函数签名不一致,导致派生类的接口并没有覆盖掉基类。这样也可以通过编译,但是程序运行效果肯定不对,后期查起来非常痛苦。因此不如让编译器来做多些工作。

派生类中虚函数的实现:

跟基类的区别是增加了折扣因子。

然后让我们看看主程序的情况:

Calc函数接受基类引用,会发生动态绑定。

在main里首先建立一个Fruit对象,再建立一个Apple对象,然后调用函数看结果。

注意最后的两个数。
Calc函数从字面上看调的是Fruit的接口,当我们传入Fruit引用时确实是这样。当我们传入的是Apple时,程序“智能”地调用了Apple接口而不是Fruit接口,这个就是动态绑定in action。


有了动态绑定这个强大的武器后,我们再介绍一些套路,就可以开始初级OOP了。

第一步我们会着手设计类体系,确定好基类,通常是抽象基类。

所谓抽象基类就是含有纯虚函数的基类。
所谓纯虚函数就是在虚函数声明的最后加上一个=0。纯虚函数不需要实现,因此可知抽象基类是没法实例化的,即无法用抽象基类建立对象。没有实现怎么建立对象呢?
为什么需要抽象基类?这是因为我们并不需要把概念实例化,刚才的Fruit类其实就可以设定为抽象基类。我们需要的只是抽象基类规定好接口,后续的派生类来覆盖即可。
至于如何从现实的问题推导出类体系,这是门大学问,甚至可以说是一门艺术,单独放到后面去讲解。

第二步实现类体系

第三步设计辅助类来应用类体系

C++的OOP有一个悖论:我们无法直接使用对象进行OOP,因为动态绑定只对指针和引用有效。我们通常会在辅助类中存放指向基类的智能指针,然后在辅助类中调用基类接口来完成需求。
注意是智能指针而不是指针,有了智能指针后可以干掉new和delete了。

第四步在主程序中使用这些辅助类完成程序功能

 

这样说起来有些抽象,让我们使用数据抽象、继承和动态绑定来重新实现水果商人问题,见《OOP实例-水果商人(一)》。