1. 虚函数
在类的定义中,前面带有关键字virtual
的成员函数称为虚函数。如下形式:
class base{
virtual int get(); // 虚函数
}
int base::get(){}
对于虚函数,需要注意的几点是:
virtual
关键字只用在类定义里的函数声明中,函数的具体实现时不使用该关键字;- 构造函数和静态成员函数(带有
static
关键字)不能是虚函数。
以上是虚函数的简要内容,后面部分会介绍关于虚函数的详细内容。
2. 多态
在 C + + {\rm C++} C++中,多态存在于类与类之间存在继承关系时的情况。多态的实质是在调用成员函数时,根据调用函数的对象的不同类型来执行不同的函数。多态的表现形式之一(通过基类的指针实现)如下:
- 派生类的指针可以赋给基类指针;
- 通过基类指针调用基类和派生类中的同名虚函数时:若该指针指向一个基类的对象,那么被调用的是基类的虚函数;若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。这就是上述提到的根据对象的类型来执行不同的函数。
上面仅用文字介绍了多态的内容,下面以一个实例来说明多态机制:
class CBase {
public:
virtual void SomeVirtualFunction() {}
};
class CDerived :public CBase
{
public:
virtual void SomeVirtualFunction() {}
};
int main() {
CDerived ODerived;
CBase* p = &ODerived;
// 调用哪一个虚函数取决于p所指向的内容
// 具体到该程序,该虚函数来自于派生类
p->SomeVirtualFunction();
return 0;
}
上述指针p
指向派生类CDerived
的对象,所以p
调用的是派生类CDerived
的虚函数。上面介绍的是多态的其中一种形式,多态还存在其他的表现形式:
- 派生类的对象可以赋给基类的引用;
- 通过基类引用调用基类和派生类中的同名虚函数时:如该引用引用的是一个基类的对象,那么被调用的是基类的虚函数;若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。
class CBase {
public:
virtual void SomeVirtualFunction() {}
};
class CDerived :public CBase
{
public:
virtual void SomeVirtualFunction() {}
};
int main() {
CDerived ODerived;
CBase& r = ODerived;
// 调用哪一个虚函数取决于r所引用的内容
// 具体到该程序,该虚函数来自于派生类
r.SomeVirtualFunction();
return 0;
}
上面是多态的两种典型形式,接下来来看一个综合的例子:
class A {
public:
virtual void Print() {
cout << "A::Print" << endl;
}
};
class B :public A {
public:
virtual void Print() {
cout << "B::Print" << endl;
}
};
class C :public A {
public:
virtual void Print() {
cout << "C::Print" << endl;
}
};
class D :public B {
public:
virtual void Print() {
cout << "D::Print" << endl;
}
};
int main() {
// 为每一个类定义一个对象
A a;
B b;
C c;
D d;
// 为每一个对象定义一个指针
A* pa = &a;
B* pb = &b;
C* pc = &c;
D* pd = &d;
// 输出语句
pa->Print(); // A::Print
pa = pb;
pa->Print(); // B::Print
pa = pc;
pa->Print(); // C::Print
pa = pd;
pa->Print(); // D::Print
return 0;
}
我们可以从以上程序总结出多态的作用:在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。下一部分将用两个实际例子来说明多态的用法和作用。
3. 基于多态的实例
本部分介绍基于多态实现的两个 C + + {\rm C++} C++案例,二者均来自参考部分的课程。
3.1 魔法门之英雄无敌
游戏介绍请参考百度百科。现在,游戏中有很多种怪物,每种怪物都是一个类与之对应,每个具体的怪物就是一个类的对象。如士兵类CSoldier
、凤凰类CPhonex
、狼类CWolf
、龙类CDragon
和天使类CAngel
。并且,怪物间能够相互攻击,攻击敌人和被攻击时都有相应的动作,而这些动作都是通过类的成员函数实现。每个类的成员变量对应于怪物的属性,如生命值、攻击力等。在游戏版本升级时,要增加新的怪物——雷鸟。这就要考虑如何编写代码使得在新添加一个类CThunderBird
时,程序总体的改动很小。
这里,为每个怪物编写三个表示动作的成员函数,Attack
、FightBack
和Hurted
。Attack
表示攻击动作,在调用Attack
函数时对应于调用被攻击者的Hurted
函数,被攻击者的生命值减少,同时也调用被攻击者的FightBack
的函数,表示对攻击者的反击;FightBack
表示被攻击时的反击动作;Hurted
表示被攻击时的动作,同时减少生命值。这里,我们创建基类CCreature
,所有怪物类都继承自该基类。
// 基类CCreature
class CCreature {
protected:
int m_nLifeValue, m_nPower; // 怪物的生命值和攻击力
public:
// 多态,将成员函数定义为虚函数
virtual void Attack(CCreature* pCreature) {} // 攻击动作
virtual void Hurted(int nPower) {} // 受伤动作
virtual void FightBack(CCreature* pCreature) {} // 反击动作
};
// 我们以其中一个类举例说明
class CSoldier :public CCreature {
public:
// 多态
virtual void Attack(CCreature* pCreature) {}
virtual void Hurted(int nPower) {}
virtual void FightBack(CCreature* pCreature) {}
};
void CSoldier::Attack(CCreature* p) {
// ...
// 该处写表示攻击动作的代码
// ...
p->Hurted(m_nPower); // p表示被攻击对象
p->FightBack(this); // 被攻击对象反击
}
void CSoldier::Hurted(int nPower) {
// ...
// 该处写表示受伤动作的代码
// ...
m_nLifeValue -= nPower; // 被攻击时,生命值减少
}
void CSoldier::FightBack(CCreature* p) {
// ...
// 该处写表示反击动作的代码
// ...
p->Hurted(m_nPower / 2); // 以攻击者的1/2攻击力作为被攻击者反击时的攻击力
}
由上述代码可知,当我们需要增加一个CThunderBird
类时,仅在类中改变基类的对应成员函数的实现方式即可,而不必修改原来的代码。上面代码的使用方式如下:
// 声明怪物对象
CSoldier Soldier; CPhonex Phonex; CWolf Wolf;
// 士兵攻击凤凰和狼
Soldier.Attack(& Phonex);
Soldier.Attack(& Wolf);
void CSoldier::Attack(CCreature* p) {
// ...
// 该处写表示攻击动作的代码
// ...
p->Hurted(m_nPower); // p表示被攻击对象
p->FightBack(this); // 被攻击对象反击
}
观察上面士兵攻击凤凰和狼的代码部分,我们在前面提到,通过基类引用调用基类和派生类中的同名虚函数时:如该引用引用的是一个基类的对象,那么被调用的是基类的虚函数;若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数。这里在语句Soldier.Attack(& Phonex)
,对应于函数中的语句p->Hurted(m_nPower)
,由于这里p
表示对象Phonex
,所以这里调用的Hurted
和FightBack
都是CPhonex
类的成员函数,这就是多态的实现。
3.2 几何形体处理程序
几何形体处理程序实现的功能如下,输入若干个几何形体的参数,要求按面积排序输出,并且在输出中指明具体的几何形体。输入的规则如下:输入的第一行表示几何形体的数目 n {\rm n} n,接着下面输入 n {\rm n} n行值,每一行以一个字母 c {\rm c} c开头。如果 c {\rm c} c是 ′ R ′ {\rm 'R'} ′R′,则表示一个矩形,本行后面跟两个整数分别表示举行的宽和高;若干 c {\rm c} c是 ′ C ′ {\rm 'C'} ′C′,则表示一个圆,本行后面跟一个整数表示圆的半径;如果 c {\rm c} c是 ′ T ′ {\rm 'T'} ′T′,则表示一个三角形,本行后面紧跟三个整数表示三角形三条边的长度。输出的规则如下:按面积从小到大依次输出每个几何形体的种类及面积。每行输出一个几何形体,输出的具体格式为 形体名称:面积。如:
// 输入
3 // 表示一共输入三个几何形体
R 3 5 // 矩形,其宽和高分别为3和5
C 9 // 圆,其半径为9
T 3 4 5 // 三角形,其三条边分别为3 4 5
// 经程序处理后,每个形体的面积以此为:矩形为6,圆为254.34,三角形为15.
// 输出
Triangle: 6
Rectangle: 15
Circle: 254.34
与上一个例子相似,我们首先定义一个基类CShape
,所以几何形体类都继承自该基类。所有形体的共同点有:求面积和打印信息。而由于不同形体计算面积的方式不同,我们在各自的几何形体类中具体实现这两个函数,这就涉及到多态。
// 基类
class CShape {
public:
virtual double Area() = 0; // 纯虚函数
virtual void PrintInfo() = 0;
};
// 矩形类
class CRectangle :public CShape {
public:
int w, h;
virtual double Area();
virtual void PrintInfo();;
};
// 圆类
class CCircle :public CShape {
public:
int r;
virtual double Area();
virtual void PrintInfo();;
};
// 三角形类
class CTriangle :public CShape {
public:
int a, b, c;
virtual double Area();
virtual void PrintInfo();;
};
// 矩形
double CRectangle::Area() {
return w * h;
}
void CRectangle::PrintInfo() {
cout << "Rectangle: " << Area() << endl;
}
// 圆
double CCircle::Area() {
return 3.14 * r * r;
}
void CCircle::PrintInfo() {
cout << "Circle: " << Area() << endl;
}
// 三角形
double CTriangle::Area() {
double p = (a + b + c) / 2.0;
return sqrt(p * (p - a) * (p - b) * (p - c));
}
void CTriangle::PrintInfo() {
cout << "Triangle: " << Area() << endl;
}
在上面程序中,由于在基类中不涉及计算面积和输出信息的具体实现,我们将对应成员函数定义为纯虚函数。同时,由于我们需要对输出结果按面积排序,我们先定义自己的比较函数以作为函数qsort
的参数。qsort
比较函数的某一位参数可以让我们自定义排序的类型,具体的写法可参考网上相关资料。
// 直接声明为n的最大值
CShape* pShapes[100];
// 由于输出结果需要对面积排序,这里我们定义比较函数
int MyCompare(const void* s1, const void* s2) {
// 我们注意到MyCompare参数的类型是void*,所以其不能直接位于赋值
// 运算符的右边。我们再使用一个*来取对应的值,即**s。
double a1, a2;
CShape** p1;
CShape** p2;
// 强制类型转换
p1 = (CShape**)s1;
p2 = (CShape**)s2;
a1 = (*p1)->Area();
a2 = (*p2)->Area();
// 根据面积的大小关系返回不同的值
if (a1 < a2) {
return -1;
}
else if (a1 > a2) {
return 1;
}
else
{
return 0;
}
}
下面是主函数的实现:
int main() {
int n;
CRectangle* pr; CCircle* pc; CTriangle* pt;
// 从键盘输入总的几何形体数
cin >> n;
// 使用循环输入剩余内容
for (int i = 0; i < n; ++i) {
char c;
// 根据输入字符确定几何形体
cin >> c;
switch (c)
{
case 'R':
pr = new CRectangle();
// 输入宽和高
cin >> pr->w >> pr->h;
pShapes[i] = pr;
break;
case 'C':
pc = new CCircle();
// 圆的半径
cin >> pc->r;
pShapes[i] = pc;
break;
case 'T':
pt = new CTriangle();
// 输入三角形三条边的边长
cin >> pt->a >> pt->b >> pt->c;
pShapes[i] = pt;
break;
default:
break;
}
}
// 对结果排序
qsort(pShapes, n, sizeof(CShape*), MyCompare);
// 输出
for (int i = 0; i < n; ++i) {
pShapes[i]->PrintInfo();
}
return 0;
}
注意,派生类和基类中的同名同参数表的函数,不加virtual
关键字也自动成为虚函数。最后给出在构造函数和析构函数中调用虚函数的性质:
- 在非构造函数和非析构函数的成员函数中调用虚函数,即是多态;
- 在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用的函数是自己的还是派生类的。
4. 总结
由前面的内容,我们可以总结出多态的相关内容:使用多态时,程序不必为每一个派生类都编写函数调用,派生类可以只关注本类中对应函数的实现功能,做到基类函数的同一接口的不同实现方式,大大提高了程序的可复用性;同时,基类也可以调用派生类的成员,即向后兼容,提高了程序的可扩充性和可维护性。后一点内容我们在以后的文章中会介绍。总之,多态是面向过程编程和面向对象编程的显著差异之一。灵活使用多态对于编写健壮、可扩充、可复用的程序至关重要。
参考
- 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.