(八)C++学习 | 虚函数和多态的基本概念


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时,程序总体的改动很小。

这里,为每个怪物编写三个表示动作的成员函数,AttackFightBackHurtedAttack表示攻击动作,在调用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,所以这里调用的HurtedFightBack都是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. 总结

由前面的内容,我们可以总结出多态的相关内容:使用多态时,程序不必为每一个派生类都编写函数调用,派生类可以只关注本类中对应函数的实现功能,做到基类函数的同一接口的不同实现方式,大大提高了程序的可复用性;同时,基类也可以调用派生类的成员,即向后兼容,提高了程序的可扩充性和可维护性。后一点内容我们在以后的文章中会介绍。总之,多态是面向过程编程和面向对象编程的显著差异之一。灵活使用多态对于编写健壮、可扩充、可复用的程序至关重要。


参考

  1. 北京大学公开课:程序设计与算法(三)C++面向对象程序设计.


  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值