C++多态的好处和作用(游戏程序实例)

http://c.biancheng.net/view/265.html

在面向对象的程序设计中,使用多态能够增强程序的可扩充性,即程序需要修改或增加功能时,只需改动或增加较少的代码。此外,使用多态也能起到精简代码的作用。本节通过游戏实例来说明多态的作用。
游戏程序实例
游戏软件的开发最能体现面向对象设计方法的优势。游戏中的人物、道具、建筑物、场景等都是很直观的对象,游戏运行的过程就是这些对象相互作用的过程。每个对象都有自己的属性和方法,不同对象也可能有共同的属性和方法,特别适合使用继承、多态等面向对象的机制。下面就以“魔法门”游戏为例来说明多态在增加程序可扩充性方面的作用。

“魔法门”游戏中有各种各样的怪物,如骑士、天使、狼、鬼,等等。每个怪物都有生命力、攻击力这两种属性。怪物能够互相攻击。一个怪物攻击另一个怪物时,被攻击者会受伤;同时,被攻击者会反击,使得攻击者也受伤。但是一个怪物反击的力量较弱,只是其自身攻击力的 1/2。

怪物主动攻击、被敌人攻击和实施反击时都有相应的动作。例如,骑士的攻击动作是挥舞宝剑,而火龙的攻击动作是喷火;怪物受到攻击会嚎叫和受伤流血,如果受伤过重,生命力被减为 0,则怪物就会倒地死去。

针对:这个游戏,该如何编写程序,才能使得游戏版本升级、要增加新的怪物时,原有的程序改动尽可能少呢?换句话说,如何才能使程序的可扩充性更好呢?

然而,无论是否使用多态,均应使每种怪物都有一个类与之对应,每个怪物就是一个对象。而且,怪物的攻击、反击和受伤等动作都是通过对象的成员函数实现的,因此需要为每个类编写 Attack、FightBack 和 Hurted 成员函数。

Attack 成员函数表现攻击动作,攻击某个怪物并调用被攻击怪物的 Hurted 成员函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack 成员函数,遭受被攻击怪物的反击。

Hurted 成员函数减少自身生命值,并表现受伤动作。

FightBack 成员函数表现反击动作,并调用被反击对象的 Hurted 成员函数,使被反击对象受伤。

下面对比使用多态和不使用多态两种写法,来看看多态在提高程序可扩充性方面的作用。

先介绍不用多态的写法。假定用 CDmgon 类表示龙,用 CWolf 类表示狼,用 CGhost 类表示鬼,则 CDragon 类的写法大致如下(其他类的写法与之类似):

class CDragon
{
private:
	int power;  //攻击力
	int lifeValue;  //生命值
public:
	void Attack(CWolf * p);  //攻击“狼”的成员函数
	void Attack(CGhost* p);  //攻击“鬼”的成员函数
	//……其他 Attack 重载函数
	//表现受伤的成员函数
	void Hurted(int nPower);
	void FightBack(CWolf * p);  //反击“狼”的成员函数
	void FightBack(CGhost* p);  //反击“鬼”的成员函数
	//......其他FightBack重载函数
};
各成员函数的写法如下:
void CDragon::Attack(CWolf* p)
{
	p->Hurted(power);
	p->FightBack(this);
}
void CDragon::Attack(CGhost* p)
{
	p->Hurted(power);
	p->FightBack(this);
}
void CDragon::Hurted(int nPower)
{
	lifeValue -= nPower;
}
void CDragon::FightBack(CWolf* p)
{
	p->Hurted(power / 2);
}
void CDragon::FightBack(CGhost* p)
{
	p->Hurted(power / 2);
}

第 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 种怪物。当然,也会有 71 个 FightBack 成员函数(这里假设两条龙也能互相攻击)。对于其他类,如 CWolf 类等,也是这样。

以上为非多态的实现方法。如果游戏版本升级,增加了新的怪物“雷鸟”,假设其类名为 CThunderBird,则程序需要做哪些改动呢?

除了编写一个 CThiinderBird 类外,所有的类都需要增加以下两个成员函数,用以对“雷鸟”实施攻击,以及在被“雷鸟”攻击时对其进行反击:

void Attack(CThunderBird* p);
void FightBack(CThunderBird* p);

这样,在怪物种类多的时候,工作量会比较大。

实际上,在非多态的实现中,使代码更精简的做法是将 CDragon、CWolf 等类的共同特点 抽取出来,形成一个 CCreature 类,然后再从 CCreature 类派生出 CDragon、CWolf 等类。但是由于每种怪物进行攻击、反击和受伤时的表现动作不同,CDmgon、CWdf 这些类还要实现各自的 Hurted 成员函数,以及一系列 Attack、FightBack 成员函数。因此,如果没有利用多态机制,那么即便引人基类 CCreature,对程序的可扩充性也没有太大帮助。

下面再来看看,如果使用多态机制编写这个程序,在要新增 CThunderBird 类时,程序改动的情况。使用多态的写法如下:设置一个基类 CCreature,概括所有怪物的共同特点。所有具体的怪物类,如 CDragon、CWolf、CGhost 等,均从 CCreature 类派生而来。下面是 CCreature 类的写法:

class CCreature {  //“怪物”类
protected:
    int lifeValue, power;
public:
    virtual void Attack(CCreature* p) {};
    virtual void Hurted(int nPower) {};
    virtual void FightBack(CCreature* p) {};
};

可以看到,基类 CCreature 中只有一个 Attack 成员函数,也只有一个 FightBack 成员函数。

实际上,所有 CCreature 类的派生类也都只有一个 Attack 成员函数和一个 FightBack 成员函数。例如,CDragon 类的写法如下:

class CDragon : public CCreature
{
public:
    virtual void Attack(CCreature* p) {
        p->Hurted(power);
        p->FightBack(this);
    }
    virtual int Hurted(int nPower) {
        lifeValue -= nPower;
    }
    virtual int FightBack(CCreature* p) {
        p->Hurted(power / 2);
    }
};

CDragon 类的成员函数中省略了表现动作和声音的那部分代码。其他类的写法和 CDragon 类类似,只是实现动作和声音的代码不同。如何实现动画的动作和声音不是本书要讲述的内容。

在上述多态的写法中,当需要增加新怪物“雷鸟”时,只需要编写新类 CThunderBird 即可,不需要在已有的类中专门为新怪物增加 void Attack(CThunderBird * p) 和 void FightBack(CThunderBird* p) 这两个成员函数。也就是说,其他类根本不用修改。这样一来,和前面非多态的实现方法相比,程序的可扩充性当然大大提高了。实际上,即便不考虑可扩充性的问题,程序本身也比非多态的写法大大精简了。

为什么 CDragon 等类只需要一个 Attack 函数,就能够实现对所有怪物的攻击呢?

假定有以下代码片段:

CDragon dragon;
CWolf wolf;
CGhost ghost;
CThunderBird bird;
Dragon.Attack(&wolf);
Dragon.Attack(&ghost);
Dragon.Attack(&bird);

根据赋值兼容规则,上面第 5、6、7 行中的参数都与基类指针类型 CCreature* 相匹配,所以编译没有问题。从 5、6、7 三行进入 CDragon::Attack 函数后,执行 p-> Hurted(power) 语句时,p 分别指向的是 wolf、ghost 和 bird,根据多态的规则,分别调用的就是 CWolf::Hurted、CGhost::Hurted 和 CBird: Hurted 函数。

FightBack 函数的情况和 Attack 函数类似,不再赘述。

  • 7
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: 下面是一个更加完整的C++多态实例,其定义了一个基类Shape和两个派生类Circle和Rectangle,它们都实现了基类的虚函数area()和draw(),并且在main函数通过基类指针传入不同的派生类实例并调用它们的虚函数: ```c++ #include <iostream> using namespace std; class Shape { public: virtual double area() = 0; virtual void draw() = 0; }; class Circle : public Shape { private: double radius; public: Circle(double r) { radius = r; } double area() { return 3.14 * radius * radius; } void draw() { cout << "Drawing a circle..." << endl; } }; class Rectangle : public Shape { private: double width, height; public: Rectangle(double w, double h) { width = w; height = h; } double area() { return width * height; } void draw() { cout << "Drawing a rectangle..." << endl; } }; int main() { Shape* pShape; Circle circle(5); Rectangle rectangle(3, 4); pShape = &circle; cout << "Circle area: " << pShape->area() << endl; pShape->draw(); pShape = &rectangle; cout << "Rectangle area: " << pShape->area() << endl; pShape->draw(); return 0; } ``` 输出结果如下: ``` Circle area: 78.5 Drawing a circle... Rectangle area: 12 Drawing a rectangle... ``` 可以看到,在传入不同的派生类实例时,基类指针可以调用对应的派生类虚函数实现,实现了多态的效果。 ### 回答2: 多态是面向对象的一个重要特性,它允许我们通过父类的指针或引用来操作子类的对象。这种多态的使用方式可以提高代码的灵活性和可复用性。 当一个子类对象传入父类指针的实例时,父类指针将指向子类对象的地址。此时,如果通过父类指针来调用虚函数,将会根据对象的实际类型来执行对应的子类方法。这就是多态。 假设有一个Animal类作为父类,有两个子类Dog和Cat继承了Animal类。我们可以创建一个Animal类型的指针,然后将一个Dog或Cat类的对象传入该指针。这样,在后续的代码,我们可以通过该指针调用Animal类的方法,编译器会根据实际的子类对象类型来动态决定是调用Dog还是Cat的方法。 例如,我们可以创建一个Animal指针,然后用Dog类的对象来初始化该指针。然后通过该指针调用Animal类的虚函数,实际会执行Dog类的虚函数。 这种多态的使用方式非常有用,可以在编写通用的代码时,将关注点放在父类上,而不需要关心具体的子类类型。这样可以提高代码的可扩展性和可维护性,减少了代码的重复和冗余。 总之,多态实例传入父类指针实例,是实现多态性的一种常见方式。通过父类指针来操作子类对象,可以以父类的角度来处理不同子类的对象,提高代码的灵活性和可复用性。 ### 回答3: 多态是面向对象编程的一个重要概念,它允许一个对象能够同时具备多个类型,即在不修改代码的情况下,能够通过父类的指针或引用来操作子类的对象。在C++,可以通过继承和虚函数来实现多态。 当一个子类对象传递给一个父类指针实例时,会发生隐式类型转换,并且只能调用父类定义的成员函数。这是因为父类指针指向的是子类对象的基类部分,而基类只能访问自己的成员函数和数据成员。这种情况下,如果子类重写了父类的虚函数,那么在通过父类指针调用该函数时,会根据实际对象的类型来调用相应的函数。 例如,有一个父类Animal和两个子类Dog和Cat。Animal类有一个虚函数speak(),并且Dog和Cat类分别重写了这个函数。那么当我们通过Animal*指针指向一个Dog对象时,调用speak()函数,会输出“汪汪”,因为实际指向的是Dog类重写后的函数。同理,如果将Animal*指针指向一个Cat对象,调用speak()函数,会输出“喵喵”。 实例传入父类指针实例可以在很多场景使用。比如,我们可以定义一个接口类A,然后定义多个实现类B、C、D等,并且它们都继承自A。当我们需要使用这些实现类的时候,可以通过A*指针来传递不同的实例对象,从而实现对于不同实例的统一操作和处理。 总之,多态实例传入父类指针实例可以提高代码的灵活性和可扩展性,使得程序设计更加符合面向对象的思想。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值