APP下载

用游戏编程实例进行C++“多态”概念教学

2011-12-31郭炜

计算机教育 2011年14期

  摘要:“多态”是面向对象程序设计方法中的重要概念,也是提高程序可扩充性的重要手段。然而初学面向对象编程的学生往往难以真正体会到其作用。文章介绍一个在教学中沿用多年,能够生动而充分地展示多态的作用,并在教学比赛中获奖的游戏编程教学案例,供大家参考。
  关键词:多态;可扩充性;虚函数;抽象类
  
  1问题的提出
  面向对象程序设计语言有封装、继承和多态三种机制,这三种机制能够有效提高程序的可读性、可扩充性和可重用性。
  而“多态”(Polymorphism),可以分为编译时的多态和运行时的多态。
  编译时的多态,主要指的是运算符的重载和函数的重载。这部分内容,比较简单,易于理解,本文并不打算讨论。
  运行时的多态,指的是以下机制(本文以后提到的“多态”,都指的是运行时的多态):
  对于通过基类指针,调用基类和派生类中都有的同名、同参数表的虚函数这样的语句,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果该基类指针指向的是一个基类对象,则基类的虚函数被执行,如果该基类指针指向的是一个派生类对象,则派生类的虚函数被执行(将上面表述中的“指针”换成“引用”,同样成立)。
  多态可以简单地理解成同一条函数调用语句能调用不同的函数,或者说,对不同对象发送同一消息,使得不同对象有各自不同的行为。
  多态在面向对象的程序设计语言中是如此重要,以至于有类和对象的概念,但是不支持多态的语言,只能被称作“基于对象的程序设计语言”,而不能被称为“面向对象的程序设计语言”。如Visual Basic就是“基于对象的程序设计语言”。
  让学生掌握多态的语法规则并不难,难的是让他们深刻理解多态到底有什么用处。实际上,在面向对象的编程中使用多态,能够有效地提高程序的可扩充性,这就是多态最大的作用。
  所谓一个程序的可扩性好,指的就是当该程序的功能需要增加或修改时,只需改动或增加比较少的代码就能实现。往往,一个程序员只有在编写了一定规模的程序,并且等到其程序真正需要添加新功能的时候,才能切身体会到程序的可扩充性是多么重要。那么,怎样才能让没有多少编程经历的低年级学生,不需要编写大规模的程序就能体会到多态在提高程序可扩充性方面的作用呢?这就是本文要探讨的问题。
  2问题的现状
  笔者查阅多本流行的C++教材,这些教材和讲义大多对多态提高程序的可扩充性这个作用未能充分展示。这些教材在阐述多态时,所举的例子一般都是这样的:
  开设一个基类指针数组,该数组里的指针,有的指向基类对象,有的指向派生类对象。在此种情况下,遍历该数组,对每个数组元素,均通过它去调用基类和派生类里都有的同名虚函数,这就达到了在每个对象上都执行它自己的虚函数的目的[2]。例如,一个几何形体演示程序,有基类Shape,还有Rectangle,Triangle和Circle等Shape的派生类,这些类都有虚函数double Area()用以计算图形的面积。那么要计算所有几何图形的面积,只需用一个Shape * 类型的数组,存放所有几何图形对象的地址,然后遍历该数组,对每个元素(即类型为Shape * 的变量)均通过它去调用Area()虚函数,那么多态机制就能确保每个几何图形的面积都是用正确的Area()函数计算出来的[3]。
  这样的例子,说明使用多态能够某种程度上精简程序的代码,但不能很好地说明多态在增强可扩充性方面的作用。比较好的例子应该是用多态和非多态的方法各写一段程序,然后要求对该程序进行功能上的扩充,此时再来看这两段程序各要做多大的改动——这才能够充分体现多态的优势。
  笔者看到的教材里,只有一部采用了这样的写法[1]。该书举了一个异质链表(同一链表里存放不同类型的对象)的例子。该例子能够充分说明多态的优点,但是略显冗长,不够生动有趣,也不像实践中的例子。
  那么软件开发的实践中,能否找到生动有趣而又不冗长的例子,来充分说明多态在程序可扩充性方面的作用呢?答案是肯定的,那就是到游戏开发中去寻找案例。
  3问题的解决
  游戏软件的开发,是最能体现面向对象设计方法的优势的。游戏中的人物、道具、建筑物、场景,都是很直观的对象,游戏运行的过程,就是这些对象相互作用的过程。每个对象都有自己的属性和方法,不同对象又可能有共同的属性和方法,特别适合使用继承、多态等面向对象的机制。而且,游戏本来就是学生所津津乐道的,在课堂的PPT里放几张游戏的截图,学生精神就会为之一振,兴趣大增。因此,笔者在讲述“多态”这一概念的时候,以“魔法门之英雄无敌”游戏的开发为例,充分论述了多态在提高程序可扩充性方面的作用,让同学们不但能学得明白,还能学得有趣。
  “魔法门”游戏中有各种各样的怪物,如骑士、天使、狼,鬼,等等。每个怪物都有生命力、攻击力这两种属性。怪物能够互相攻击,一个怪物攻击另一个怪物时,会使被攻击者受伤;同时被攻击者会反击,使得攻击者也受伤。但是一个怪物反击的力量较弱,只是其自身攻击力的1/2。
  怪物主动攻击、被敌人攻击和实施反击时都有相应的动作。比如骑士攻击时的动作就是挥舞宝剑,而火龙的攻击动作就是喷火;怪物受到攻击会嚎叫和受伤流血,如果受伤过重,生命力被减为0,则怪物就会倒地死去…….
  针对这个游戏,教师提出的问题是:该如何编写程序,才能使得游戏版本升级,要增加新的怪物时,原有的程序改动尽可能少 。换句话说,就是怎样才能使程序的可扩充性更好。
  显然,不论是否使用多态,均应使每种怪物都有一个类与之对应,每个怪物就是一个对象。而且,怪物的攻击、反击和受伤等动作,都是通过对象的成员函数实现的,因此为每个类都需要编写Attack、FightBack和 Hurted成员函数
  Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的 Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack成员函数,遭受被攻击怪物反击。
  Hurted函数减少自身生命值,并表现受伤动作。
  FightBack成员函数表现反击动作,并调用被反击对象的Hurted成员函数,使被反击对象受伤。
  接下来就是对比使用多态和不使用多态两种写法,来体现多态在提高程序可扩充性方面的作用。
  先看不用多态的写法。假定用“CDragon”类表示火龙,用“CWolf”类表示狼,用“CGhost”类表示鬼,则“CDragon”类写法大致如下(其他类的写法也类似):
  
  class CDragon
  {
  private:
  int m_nPower ; //攻击力
  int m_nLifeValue ; //生命值
  public:
  //攻击“狼”的成员函数
  void Attack(CWolf * p);
  //攻击“鬼”的成员函数
  void Attack(CGhost * p);
  //......其他Attack重载函数
  //表现受伤的成员函数
  void Hurted( int nPower);
  //反击“狼”的成员函数
  void FightBack(CWolf * p);
  //反击“鬼”的成员函数
  void FightBack(CGhost * p);
  
  //......其他FightBack重载函数
  };
  
  接下来再看各成员函数的写法:
  
  1.void CDragon::Attack(CWolf * p)
  2.{
  3.p->Hurted(m_nPower);
  4. p->FightBack(this);
  5.}
  6.void CDragon::Attack(CGhost * p)
  7.{
  8. p->Hurted(m_nPower);
  9. p->FightBack(this);
  10.}
  11.void CDragon::Hurted(int nPower)
  12.{
  13.m_nLifeValue -= nPower;
  14.}
  15.void CDragon::FightBack(CWolf * p)
  16.{
  17. p->Hurted(m_nPower/2);
  18.}
  19.void CDragon::FightBack(CGhost * p)
  20.{
  21.p->Hurted(m_nPower/2);
  22.}
  
  在上面带行号的程序中:
  第1行,Attack函数的参数p,指向被攻击的CWolf对象。
  第3行,在p所指向的对象上面执行Hurted成员函数,使被攻击的“狼”对象受伤。调用Hurted时,参数是攻击者“龙”对象的攻击力。
  第4行,以指向攻击者自身的this指针为参数,调用被攻击者的FightBack成员函数,接受被攻击者的反击。
  显然,在真实的游戏程序中,CDragon类的Attack成员函数中还应包含表现火龙在攻击时的动作和声音的代码。
  第13行,一个对象的Hurted成员函数被调用会导致该对象的生命值减少,减少的量等于攻击者的攻击力。当然,真实的程序中,Hurted函数还应包含表现受伤时动作的代码,以及生命力如果减至小于等于零,则倒地死去的代码。
  第17行,p指向的是实施攻击者,对攻击者进行反击,实际上就是调用攻击者的Hurted成员函数使其受伤。其受到的伤害的大小,等于实施反击者的攻击力的一半(反击的力量不如主动攻击大)。当然,FightBack函数中其实也应包含表现反击动作的代码。
  实际上,如果游戏中有n种怪物,CDragon 类中就会有n个Attack成员函数,用于攻击n种怪物。当然,也会有n个FightBack成员函数(这里我们假设两条龙也能互相攻击)。对于其他类,比如CWolf等,也是这样
  以上为非多态的实现方法。如果游戏版本升级,增加了新的怪物雷鸟,假设其类名为CThunderBird, 则程序需要做哪些改动呢?
  显然,除了新写一个CThunderBird类外,所有的类都需要增加以下两个成员函数,用以对雷鸟实施攻击,以及在被雷鸟攻击时对其进行反击:
  
  void Attack( CThunderBird * p) ;
  void FightBack( CThunderBird * p) ;
  
  这样,在怪物种类多的时候,工作量就较大。
  实际上,非多态实现中,代码更精简的做法是将CDragon,CWolf等类的共同特点抽取出来,形成一个CCreature类,然后再从CCreature类派生出CDragon、CWolf等类。但是由于每种怪物进行攻击、反击和受伤时的表现动作不同,CDragon、CWolf这些类还是要实现各自的Hurted成员函数,以及一系列Attack、FightBack成员函数。所以只要没有利用多态机制,那么即便引入基类CCreature,对程序的可扩充性也无帮助。
  下面再来看看,如果使用多态机制来编写这个程序,在要新增CThunderBird类的时候,程序改动有多大。
  多态的写法如下:
  设置一个抽象类CCreature,概括了所有怪物的共同特点。然后,所有具体的怪物类,比如CDragon,CWolf,CGhost等,均从CCreature类派生而来。
  下面是CCreature类的写法:
  
  class CCreature{
  protected :
  int m_nLifeVa