C++课程学习笔记第六周:多态
前言:本文主要是根据MOOC网北大课程——《程序设计与算法(三):C++面向对象程序设计》内容整理归纳而来,整理的课程大纲详见 https://www.cnblogs.com/inchbyinch/p/12398921.html
本文介绍了多态的概念、原理和应用案例。
1 多态的概念
虚函数:
- 在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。
- virtual 关键字只用在类定义里的函数声明中,写函数体时不用。
- 派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数
多态的机制:
- 派生类的指针可以赋给基类指针。
- 通过基类指针调用基类和派生类中的同名虚函数时,若该指针指向一个基类的对象,那么被调用是基类的虚函数;若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。
注意:
- 上述是多态的机制形式一,形式二中可以将指针换成引用,同样成立;
- 多态的特征是:1.派生类对象的指针(或引用)赋给基类指针(或引用);2.该基类指针(或引用)调用虚函数(在非构造函数、非析构函数的成员函数中)。
- 在构造函数和析构函数中调用虚函数,不是多态。
//示例1:多态的两种表现形式
class CBase {
public:
virtual void SomeVirtualFunction() { cout << "This is CBase" << endl; }
};
class CDerived:public CBase {
public :
virtual void SomeVirtualFunction() { cout << "This is CDerived" << endl; }
};
//多态表现形式一:派生类指针赋给基类指针,且调用虚函数
int main() {
CDerived ODerived;
CBase * p = & ODerived;
p->SomeVirtualFunction(); //调用哪个虚函数取决于p指向哪种类型的对象
return 0;
}
//多态表现形式二:派生类对象赋给基类引用,且调用虚函数
int main() {
CDerived ODerived;
CBase & r = ODerived;
r.SomeVirtualFunction(); //调用哪个虚函数取决于r引用哪种类型的对象
return 0;
}
//示例2:诡异的多态
class Base {
public:
void fun1() { this->fun2(); } //注意此处
virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derived:public Base {
public:
virtual void fun2() { cout << "Derived:fun2()" << endl; }
};
int main() {
Derived d;
Base * pBase = & d;
pBase->fun1();
return 0;
}
//输出结果 Derived:fun2()
//解释:在fun1()中,this是基类指针,fun2是虚函数,所以是多态
//示例3:手推运行结果
class myclass {
public:
virtual void hello(){cout<<"hello from myclass"<<endl; }
virtual void bye(){cout<<"bye from myclass"<<endl;}
};
class son:public myclass{
public:
void hello(){ cout<<"hello from son"<<endl;}
son(){ hello(); }
~son(){ bye(); }
};
class grandson:public son{
public:
void hello(){ cout<< "hello from grandson" << endl;}
void bye() { cout << "bye from grandson" << endl;}
grandson() { cout << "constructing grandson" << endl;}
~grandson(){ cout << "destructing grandson" << endl;}
};
int main(){
grandson gson;
son *pson;
pson = &gson;
pson->hello();
return 0;
}
//运行结果:
//hello from son
//constructing grandson
//hello from grandson
//destructing grandson
//bye from myclass
2 多态应用案例
2.1 游戏程序实例
在游戏——魔法门之英雄无敌中,有很多种怪物,比如Dragon、Wolf、Angle、Soldier等,每种怪物都有一个类与之对应,每个怪物就是一个对象。怪物能够互相攻击,攻击敌人和被攻击时都有相应的动作,动作是通过对象的成员函数实现的。
因此常规思路可以这样:
- 每个怪物均有生命值和攻击力属性值。
- 为每个怪物类编写 Attack、FightBack和 Hurted成员函数。
- Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack成员函数,遭受被攻击怪物反击。
- Hurted函数减少自身生命值,并表现受伤动作。
- FightBack成员函数表现反击动作,并调用被反击对象的Hurted成员函数,使被反击对象受伤。
//非多态的实现方法
class class CCreature {
protected:
int nPower ; //代表攻击力
int nLifeValue ; //代表生命值
};
//各个怪物类
class CDragon:public CCreature {
public:
void Attack(CWolf * pWolf) {
...(表现攻击动作的代码)
pWolf->Hurted( nPower);
pWolf->FightBack( this);
}
void Attack(CGhost * pGhost) {
...(表现攻击动作的代码)
pGhost->Hurted( nPower);
pGohst->FightBack( this);
}
void Hurted(int nPower) {
...(表现受伤动作的代码)
nLifeValue -= nPower;
}
void FightBack(CWolf * pWolf) {
...(表现反击动作的代码)
pWolf ->Hurted( nPower / 2);
}
void FightBack(CGhost * pGhost) {
...(表现反击动作的代码)
pGhost->Hurted( nPower / 2 );
}
};
非多态方法的缺陷:
- 有n种怪物,CDragon类中就会有n个Attack成员函数,以及n个FightBack成员函数。对于其他类也如此。
- 如果游戏版本升级,增加了新的怪物雷鸟CThunderBird,则所有的类都需要增加两个成员函数:作用在雷鸟上的Attack和FightBack函数,程序改动较大。
如果采用多态的方法,则可以:
- 对于每一种怪物,只需要写一个Attack成员函数和一个FightBack成员函数;
- 如果增加了新的怪物雷鸟CThunderBird,则只需要编写新类CThunderBird, 其他类可以原封不动,程序改动较小。
//多态的方法
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 CDragon : public CCreature {
public:
virtual void Attack(CCreature * p){
...(表现攻击动作的代码)
p->Hurted(nPower);
p->FightBack(this);
}
virtual void Hurted(int nPower) {
...(表现受伤动作的代码)
m_nLifeValue -= nPower;
}
virtual void FightBack(CCreature * p){
...(表现受伤动作的代码)
p->Hurted(m_nPower/2); //多态
}
};
具体调用过程是:
CDragon Dragon; CWolf Wolf; CGhost Ghost; CThunderBird Bird;
Dragon.Attack( & Wolf); //(1)
Dragon.Attack( & Ghost); //(2)
Dragon.Attack( & Bird); //(3)
//上面的(1),(2),(3)进入到CDragon::Attack函数后,能分别调用:
//CWolf::Hurted
//CGhost::Hurted
//CBird::Hurted
2.2 几何形体处理程序
几何形体处理程序:输入若干个几何形体的参数,要求按面积排序输出。输出时要指明形状。
Input:
- 第一行是几何形体数目n(不超过100),下面有n行,每行以一个字母c开头
- 若 c 是‘R’,则代表一个矩形,本行后面跟着两个整数,分别是矩形的宽和高
- 若 c 是‘C’,则代表一个圆,本行后面跟着一个整数代表其半径
- 若 c 是‘T’,则代表一个三角形,本行后面跟着三个整数,代表三条边的长度
Output:
- 按面积从小到大依次输出每个几何形体的种类及面积
- 每行一个几何形体,输出格式为:形体名称:面积
//程序示例
Sample Input:
3
R 3 5
C 9
T 3 4 5
Sample Output:
Triangle:6
Rectangle:15
Circle:254.34
//程序设计
#include <iostream>
#include <stdlib.h>
#include <math.h>
using namespace std;
//基类
class CShape{
public:
virtual double Area() = 0; //纯虚函数,无函数体{}
virtual void PrintInfo() = 0;
};
//派生类
class CRectangle:public CShape{
public:
int w,h;
virtual double Area(){ return w * h; }
virtual void PrintInfo(){ cout << "Rectangle:" << Area() << endl; }
};
class CCircle:public CShape {
public:
int r;
virtual double Area(){ return 3.14 * r * r; }
virtual void PrintInfo(){ cout << "Circle:" << Area() << endl; }
};
class CTriangle:public CShape {
public:
int a,b,c;
virtual double Area(){
double p = ( a + b + c) / 2.0;
return sqrt(p * ( p - a)*(p- b)*(p - c));
}
virtual void PrintInfo(){ cout << "Triangle:" << Area() << endl; }
};
CShape * pShapes[100];
int MyCompare(const void * s1, const void * s2);
int main(){
int i; int n;
CRectangle * pr; CCircle * pc; CTriangle * pt;
cin >> n;
for( 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;
}
}
qsort(pShapes,n,sizeof( CShape*),MyCompare);
for( i = 0;i <n;i ++)
pShapes[i]->PrintInfo();
return 0;
}
int MyCompare(const void * s1, const void * s2){
double a1,a2;
CShape * * p1 ; // s1,s2 是 void * ,不可写 “* s1”来取得s1指向的内容
CShape * * p2;
p1 = ( CShape * * ) s1; //s1,s2指向pShapes数组中的元素,数组元素的类型是CShape *
p2 = ( CShape * * ) s2; // 故 p1,p2都是指向指针的指针,类型为 CShape **
a1 = (*p1)->Area(); // * p1 的类型是 Cshape * ,是基类指针,故此句为多态
a2 = (*p2)->Area();
if( a1 < a2 )
return -1;
else if ( a2 < a1 )
return 1;
else
return 0;
}
注意:
- 如果添加新的几何形体,比如五边形,则只需要从CShape派生出CPentagon,以及在main中的switch语句中增加一个case,其余部分不变。
- 用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常用的做法。
3 多态的实现原理
为了实现同一个接口,不同的实现方式,C++采用了virtual函数和晚绑定技术来实现。
编译器在编译时,将类的成员函数编译成与类无关的独立函数,对象在调用该函数时,实际上依靠的是函数名、对象指针以及其他所需参数。所以,在派生类重写同名函数情况下,当用基类指针指向派生类对象时,该指针调用函数的话,就被编译成了该函数名、该基类指针、以及相关参数,这时找到的就是基类的函数,无法定位派生类的该函数。
为了解决这个问题,C++引入virtual函数。编译器在编译时,发现类中有虚函数,则自动插入一个一维虚函数表,里面存储了该类的各个虚函数地址。有虚函数的基类,其派生类自动继承了虚函数表,若派生类重写了虚函数,则其虚函数表中地址自动更新。有虚函数的类实例化对象时,编译器自动在对象内存开头加入一个虚表索引,其指向生成类的虚表。在编译时,若编译器发现调用的是虚函数,则自动采用晚绑定机制,即在运行时通过具体对象的虚表指针获取虚表,从而找到对应的函数。
指针的本质:
- 指针实际上就是一串地址;
- 每个指针都有类型,类型决定指针移动时的步长;
- 对指针进行类型转换,仅仅是改变了解释指针所指内存区域的方式,位模式没有变(具体地址没有变)
//示例:改变虚表指针
#include <iostream>
using namespace std;
class A {
public:
virtual void Func() { cout << "A::Func" << endl; }
};
class B:public A {
public:
virtual void Func() { cout << "B::Func" << endl; }
};
int main() {
A a;
A * pa = new B();
pa->Func();
long long * p1 = (long long * ) & a; //64位程序指针为8字节
long long * p2 = (long long * ) pa;
* p2 = * p1;
pa->Func();
return 0;
}
//运行结果:
//B::Func
//A::Func
4 相关知识点
4.1 虚析构函数
正常情况下,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。但是通过基类的指针删除派生类对象时,通常只调用基类的析构函数,这可能会导致内存泄露。
解决办法:把基类的析构函数声明为virtual(派生类的析构函数可以不进行virtual声明)。
- 一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义成虚函数。参考
- 不允许以虚函数作为构造函数。
- 构造函数和析构函数中均不能调用虚函数。
4.2 纯虚函数和抽象类
- 没有函数体的虚函数即为纯虚函数,包含纯虚函数的类叫抽象类。
- 抽象类只能作为基类来派生新类使用,不能创建独立的抽象类的对象,但抽象类的指针和引用可以指向由抽象类派生出来的类的对象。
- 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。
- 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。