多态
需求
从示例来讲C++中的多态。现在有如下需求:某游戏包含若干英雄,每个英雄有攻击动作和受伤反应。一个最蠢的实现就是定义一个英雄基类,然后再派生出若干子类,在子类中实现每个英雄的行为,需要注意的是,攻击动作会有一个目标英雄,并且攻击动作会引发目标英雄的受伤反应。那么一个最简易的实现代码如下所示。Hero.h
:
#include<string>
using namespace std;
class Hero {};
class Yi; // 为避免互相引用所必要的前置声明
class Garen :public Hero {
const string name = "Garen";
public:
void Attack(Yi* p);
void Hurted();
};
class Yi :public Hero {
const string name = "Yi";
public:
void Attack(Garen* p);
void Hurted();
};
Hero.cpp
:
#include<iostream>
#include "Hero.h"
using namespace std;
void Garen::Attack(Yi* p) {
cout << this->name << " attacks!" << endl;
p->Hurted();
}
void Garen::Hurted() {
cout << this->name << " was attacked!" << endl;
}
void Yi::Attack(Garen* p) {
cout << this->name << " attacks!" << endl;
p->Hurted();
}
void Yi::Hurted() {
cout << this->name << " was attacked!" << endl;
}
main.cpp
:
#include<iostream>
#include "Hero.h"
int main(void) {
Garen garen;
Yi yi;
garen.Attack(&yi);
yi.Attack(&garen);
return 0;
}
上述代码构成的程序是正常运行的:
Garen attacks! Yi was attacked! Yi attacks! Garen was attacked!
但是容易发现如果要新增一个英雄的话,比如现要新增一个名为”EZ”的新英雄,上述代码的改动是可预见的:所有旧英雄中都需要新增一个成员函数void Attack(EZ* p)
,并且新英雄EZ
类中需要完成对所有旧英雄的攻击代码。这样的改动量是无法接受的,注意到Hero
基类所派生的所有子类都有Attack
和Hurted
行为,只是应用对象的类不同,自然而然想到如下思路:在基类中实现,由子类继承,并在调用时重载。
问题
C++自带基类与派生类的互相转化机制,因此更改后的代码如下所示。Hero.h
:
#include<string>
using namespace std;
class Hero {
const string name = "Hero";
public:
void Attack(Hero* p);
void Hurted();
};
class Yi; // 为避免互相引用所必要的前置声明
class Garen :public Hero {
const string name = "Garen";
};
class Yi :public Hero {
const string name = "Yi";
};
Hero.cpp
:
#include<iostream>
#include "Hero.h"
using namespace std;
void Hero::Attack(Hero* p) {
cout << this->name << " attacks!" << endl;
p->Hurted();
}
void Hero::Hurted(void) {
cout << this->name << " was attacked!" << endl;
}
修改后的程序输出为:
Hero attacks! Hero was attacked! Hero attacks! Hero was attacked!
继承重载未生效。
解决
相比于普通的函数重载如int add(int a,int b)
与double add(double a,double b)
,上述代码中在父类实现的成员函数并没有指定具体的类型,而是仅以父类类型来进行限定。虽然C++可以自动完成子类到父类的自动转换,但代码的运行结果表明转换之后调用的函数访问的是父类对象的属性。
想要以父类形式调用子类中的重载函数,需要用到C++中的一个特性——虚函数。在父类中对被子类重载的函数前使用virtual
关键字修饰即可。修改后的代码如下所示,Hero.h
:
#include<string>
using namespace std;
class Hero {
const string name = "Hero";
public:
virtual void Attack(Hero* p);
virtual void Hurted();
};
class Yi; // 为避免互相引用所必要的前置声明
class Garen :public Hero {
const string name = "Garen";
public:
void Attack(Hero* p);
void Hurted();
};
class Yi :public Hero {
const string name = "Yi";
public:
void Attack(Hero* p);
void Hurted();
};
Hero.cpp
:
#include<iostream>
#include "Hero.h"
using namespace std;
void Hero::Attack(Hero* p) {}
void Hero::Hurted() {}
void Garen::Attack(Hero* p) {
cout << this->name << " attacks!"<< endl;
p->Hurted();
}
void Garen::Hurted(void) {
cout << this->name << " was attacked!" << endl;
}
void Yi::Attack(Hero* p) {
cout << this->name << " attacks!" << endl;
p->Hurted();
}
void Yi::Hurted(void) {
cout << this->name << " was attacked!" << endl;
}
原理
涉及到底层的地址与指针,待补充
接口
多态最主要的作用就是用于实现接口,接口是一个独立的函数,其可接受不同类型的对象,然后可以针对对象的类型来完成一个功能。听起来接口好像就等同于函数的重载,但实际上这里的重载不是发生在接口函数上,而是发生在基类的虚函数上。
现有如下需求,实现一台饮品制造机,要求其能制造咖啡和茶两种饮品,这两种饮品的制作过程都分为三步,前者为加咖啡、加水、加牛奶,后者为加茶业、加水、加枸杞。具体的代码实现如下,Drinking.h
:
class AbcDrinking {
public:
/*仅声明无实现的抽象函数,同时也是虚函数*/
virtual void addWater() = 0; // 加水
virtual void addMat() = 0; // 主料
virtual void addOther() = 0; // 配料
void Make();
};
class Cof :public AbcDrinking {
public:
void addWater();
void addMat();
void addOther();
};
class Tea :public AbcDrinking {
public:
void addWater();
void addMat();
void addOther();
};
Drinking.cpp
:
#include<iostream>
#include "Drinking.h"
using namespace std;
void AbcDrinking::Make() {
this->addWater();
this->addMat();
this->addOther();
}
void Cof::addWater() {
cout << "add water, ";
}
void Cof::addMat() {
cout << "add coffee, ";
}
void Cof::addOther() {
cout << "add milk." << endl;
}
void Tea::addWater() {
cout << "add mineral water, ";
}
void Tea::addMat() {
cout << "add tea, ";
}
void Tea::addOther() {
cout << "add wolfberry. " << endl;
}
main.cpp
:
#include<iostream>
#include "Drinking.h"
/*该函数构成一个接口*/
void Producer(AbcDrinking* p) {
p->Make();
}
int main(void) {
Cof cof; Tea tea;
Producer(&cof); Producer(&tea);
return 0;
}
前置声明
这是在写示例时遇到的一个坑。在声明英雄类时,英雄类之间产生了互相引用的问题,结果就是编译器一直报C2061错误。原因就在于如下代码:
class Garen :public Hero {
...
public:
void Attack(Yi* p);
...
};
class Yi :public Hero {
...
public:
void Attack(Garen* p);
...
};
这段代码不管是写在一个文件中还是分开写在多个文件中,都存在互相引用的问题。解决此问题的方式是使用前置引用:
class Yi; // 为避免互相引用所必要的前置声明
class Garen :public Hero {
...
public:
void Attack(Yi* p);
...
};
class Yi :public Hero {
...
public:
void Attack(Garen* p);
...
};