学习C++的多态性,你必然听过虚函数的概念,你必然知道有关她的种种语法,但你未必了解她为什么要那样做,未必了解她种种行为背后的所思所想。深知你不想在流于表面语法上的蜻蜓点水似是而非,今天我们就一起来揭开挡在你和虚函数(女神)之间的这一层窗户纸。
首先,我们要搞清楚女神的所作所为,即语法规范。然后再去探究她背后的逻辑道理。她的语法说来也不复杂,概括起来就这么几条:
-
在类成员方法的声明(不是定义)语句前面加个单词:virtual,她就会摇身一变成为虚函数。
-
在虚函数的声明语句末尾中加个 =0 ,她就会摇身一变成为纯虚函数。
-
子类可以重新定义基类的虚函数,我们把这个行为称之为复写(override)。
-
不管是虚函数还是纯虚函数,基类都可以为提供他们的实现(implementation),如果有的话子类可以调用基类的这些实现。
-
子类可自主选择是否要提供一份属于自己的个性化虚函数实现。
-
子类必须提供一份属于自己的个性化纯虚函数实现。
![音符动态简约分割线](https://i-blog.csdnimg.cn/blog_migrate/3930a3f12cb8d95ad0fdcdca52c6a7c2.gif)
语法都列出来了,背后的逻辑含义是什么呢?我们用一个生动的例子来说明,虚函数是如何实现多态性的。
假设我们要设计关于飞行器的类,并且提供类似加油、飞行的实现代码,考虑具体情况,飞行器多种多样,有民航客机、歼击机、轰炸机、直升机、热气球、火箭甚至窜天猴、孔明灯、纸飞机!
假设我们有一位牛得一比的飞行员,他能给各式各样的飞行器加充不同的燃料,也能驾驶各式各样的飞行器。下面我们来看看这些类可以怎么设计。
首先,飞行器。由于我们假设所有的飞行器都有两种行为:加油和飞行。因此我们可以将这两种行为抽象到一个基类中,并由它来派生具体的某款飞行器。
这是一个描述飞行器的基类,提供了两个基本的功能:加油和飞行
class aircraft
{
void refuel(); // 加燃油,普通虚函数
void fly()=0; // 飞行,纯虚函数
};
这是一个普通虚函数,意味着基类希望子类提供自己的个性化实现代码,但基类同时也提供一个缺省的虚函数实现版本,在子类不复写该虚函数的情况下作为备选方案
void aircraft::refuel()
{
// 加充通用型燃油
}
这是一个纯虚函数,意味着基类强制子类必须提供自己的个性化版本,否则编译将失败。但让人惊奇的是,C++仍然保留了基类提供该纯虚函数代码实现的权利,这也许是给千变万化的实际情况留下后路
void aircraft::fly()
{
// 一种不应该被使用的缺省飞行方案
}
有了基类aircraft,我们就可以潇洒地派生出各式各样的飞行器了,比如轰炸机和直升机:
轰炸机类定义,复写了加油和飞行
class bomber : public aircraft
{
void refuel(){} // 加充轰炸机的特殊燃油!
void fly(){} // 轰炸机实弹飞行!
};
直升机类定义,复写了飞行代码,但没有复写加油
class copter: public aircraft
{
void fly(){} // 直升机盘旋!
};
以上代码可以看到,直升机类(copter)没有自己的加油方式,直接使用了基类提供的缺省加油的方式。此时我们来定义一个能驾驭多机型的王牌飞行员类:
一个能王牌飞行员
class pilot
{
void refuelPlane(aircraft *p);
void dirvePlane(aircraft *p);
};
给我什么飞机我就加什么油
void pilot::refuelPlane(aircraft *p)
{
p->refuel();
}
给我什么飞机我就怎么飞
void pilot::dirvePlane(aircraft *p)
{
p->fly();
}
很明显,我们接下来要给这位很浪的飞行员表演一下操纵各种飞行器的机会,我们来定义各种飞机然后丢给他去处理
定义两架飞机,一架轰6K,一架武直10
aircraft *H6K = new bomber;
aircraft *WZ10 = new copter;
来一个王牌飞行员,给H6K加油(加的是轰炸机特殊燃油),并且按照H6K的特点飞行
pilot Jack;
Jack.refuelPlane(H6K); // 加充轰炸机燃油
Jack.flyPlane(H6K); // 轰炸机实弹飞行
给WZ10加油(加的是基类提供的通用燃油),按照WZ10的特点飞行
Jack.refuelPlane(WZ10); // 加充通用型燃油
Jack.flyPlane(WZ10); // 直升机盘旋
上述代码体现了最经典的所谓多态的场景,给Jack不同的飞机,就能表现不同的结果。虚函数和纯虚函数都能做到这一点,区别是,子类如果不提供虚函数的实现,那就会自动调用基类的缺省方案。而子类如果不提供纯虚函数的实现,则编译将会失败。基类提供的纯虚函数实现版本,无法通过指向子类对象的基类类型指针或引用来调用,因此不能作为子类相应虚函数的备选方案。下面给出总结。
![欧式古典 分割线](https://i-blog.csdnimg.cn/blog_migrate/9a81d76642c644dd20410c1188b09f77.png)
第一,当基类的某个成员方法,在大多数情形下都应该由子类提供个性化实现,但基类也可以提供一个备选方案的时候,请将其设计为虚函数。例如飞行器的加油动作,每种不同的飞行器原则上都应该有自己的个性化的加充然后的方式,但也不免可以有一种通用的然后和加充方式。
第二,当基类的某个成员方法,必须由子类提供个性化实现的时候,请将其设计为纯虚函数。例如飞行器的飞行动作,逻辑上每种飞行器都必须提供为其特殊设计的个性化飞行行为,而不应该有任何一种“通用的飞行方式”。
第三,使用一个基类类型的指针或者引用,来指向子类对象,进而调用经由子类复写了的个性化的虚函数,这是C++实现多态性的一个最经典的场景。
第四,基类提供的纯虚函数的实现版本,并非为了多态性考虑,因为指向子类对象的基类指针和引用无法调用该版本。纯虚函数在基类中的实现跟多态性无关,它只是提供了一种语法上的便利,在变化多端的应用场景中留有后路。
第五,虚函数和普通的函数实际上是存储在不同的区域的,虚函数所在的区域是可被覆盖(也称复写override)的,每当子类定义相同名称的虚函数时就将原来基类的版本给覆盖了,另一侧面也说明了为什么基类中声明的虚函数在后代类中不需要另加声明一律自动为虚函数,因为它所存储的位置不会发生改变。而普通函数的存储区域不会覆盖,每个类都有自己独立的区域互不相干。
最后附一幅草图以供参考