Download presentation
Presentation is loading. Please wait.
Published byIna Knutsen Modified 5年之前
1
本课件为教师郭炜参加北京市青年教师小教学基本功比赛时制作,当时获得理工组三等奖。其内容为北京大学本科生主干必修课“程序设计实习”中的一节。
程序设计实习2007 本课件为教师郭炜参加北京市青年教师小教学基本功比赛时制作,当时获得理工组三等奖。其内容为北京大学本科生主干必修课“程序设计实习”中的一节。
2
程序设计实习2007 第十六讲 虚函数和多态 http://ai.pku.edu.cn/cpp2007
3
程序设计实习2007 内容提要 问题的提出 虚函数和多态 纯虚函数和抽象类 初始问题的解决 多态的实质 多态应用的其它实例 作业
4
问题的提出 回顾上次课的作业 编写一个程序计算任给一个几何形体的面积和周长。 几何形体可以是矩形、三角形、圆形、扇形、梯形。 要求:
程序设计实习2007 问题的提出 回顾上次课的作业 编写一个程序计算任给一个几何形体的面积和周长。 几何形体可以是矩形、三角形、圆形、扇形、梯形。 要求: 1. 提示用户选择图形种类; 根据不同种类提示用户输入图形参数; 给出输入图形的面积和周长
5
作业的样例程序(假设只有矩形和圆形) class CRectangle { private: int x1,y1,x2,y2;
程序设计实习2007 作业的样例程序(假设只有矩形和圆形) class CRectangle { private: int x1,y1,x2,y2; public : int calcArea(); int calcLength(); friend ostream& operator << (ostream & output, CRectangle & rect); friend istream& operator >> (istream & input, CRectangle & rect); };
6
int CRectangle::calcArea(){ return (x2-x1)*(y2-y1); }
程序设计实习2007 int CRectangle::calcArea(){ return (x2-x1)*(y2-y1); } int CRectangle::calcLength(){ return 2*(x2-x1+y2-y1); ostream& operator << (ostream & output, CRectangle & rect){ output << "面积 = " << rect.calcArea() << ", 周长 = " << rect.calcLength() << endl; return output;
7
istream& operator >> (istream & input, CRectangle & rect){
程序设计实习2007 istream& operator >> (istream & input, CRectangle & rect){ rect.x1 = rect.y1 = rect.x2 = rect.y2 = -1; while(rect.x1==-1){ cout <<"\n请输入左上角(x1,y1)和右下角坐标(x2,y2)"; cout << endl; cout <<"要求x1,y1,x2,y2 都是非负整数并且x1<x2,y1<y2"; cout << "例如:输入 表示 左上角(5,6)"; cout << "右下角(7,8)" << endl; cin >> rect.x1 >> rect.y1 >> rect.x2 >> rect.y2; if(rect.x1<0 || rect.y1<0 || rect.x2<0 || rect.y2<0 || rect.x1>=rect.x2 || rect.y1>=rect.y2){ rect.x1 = rect.y1 = rect.x2 = rect.y2 = -1; continue; } return input;
8
int centerx ,centery, radius; public : int calcArea();
程序设计实习2007 class CCircle { private: int centerx ,centery, radius; public : int calcArea(); int calcLength(); friend ostream& operator << (ostream & output, CCircle & tri); friend istream& operator >> (istream & input, CCircle & cir); };
9
int CCircle::calcArea(){ return (int)(radius*radius*3.1415926); }
程序设计实习2007 int CCircle::calcArea(){ return (int)(radius*radius* ); } int CCircle::calcLength(){ return (int)(2*radius* ); ostream& operator << (ostream & output, CCircle & cir){ output << "面积 = " << cir.calcArea() << ", 周长 = " << cir.calcLength() << endl; return output;
10
istream& operator >> (istream & input, CCircle & cir){
程序设计实习2007 istream& operator >> (istream & input, CCircle & cir){ cir.centerx = cir.centery = cir.radius = -1; while(cir.centerx==-1){ cout <<"\n请输入圆心(x1,y1)和半径 r" << endl; cout <<"要求x1,y1,r 都是非负整数" << endl; cout << "例如:输入5 6 7 表示圆心(5,6)半径是 7 " << endl; cin >> cir.centerx >> cir.centery >> cir.radius ; if(cir.centerx<0 || cir.centery<0 || cir.radius<0){ continue; } return input;
11
case 'r': CRectangle test1;
程序设计实习2007 void main(){ char choice; cout << "请选择图形:(R - Rectangle; C - Circle; E - 结束)" << endl; cin >> choice; while(choice != 'E'){ switch(choice) { case 'R': case 'r': CRectangle test1; cin >> test1; cout << test1; break; case ‘C': case ‘c': CCircle test2; cin >> test2; cout << test2; break; } }// while
12
问题 现在要记住所有输入的几何图形,并按输入顺序输出它们的面积、周长及输入参数。 怎么改造原有的程序呢?
程序设计实习2007 问题 现在要记住所有输入的几何图形,并按输入顺序输出它们的面积、周长及输入参数。 怎么改造原有的程序呢? 每种图形分别放在各自数组中并记录每个图形的输入序号,输出时不停地在各个数组中查找 今天要介绍虚函数和多态的方法可以更好地解决这个问题。
13
一、 虚函数 在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。 class base {
程序设计实习2007 一、 虚函数 在类的定义中,前面有 virtual 关键字的成员函数就是虚函数。 class base { virtual int get() ; } int base::get() { virtual 关键字只用在类定义里的函数声明中,写函数体时不用。
14
程序设计实习2007 二、 多态的基本概念 派生类的指针可以赋给基类指针。 通过基类指针调用基类和派生类中的同名虚函数时,若该指针指向一个基类的对象,那么被调用是基类的虚函数,如果该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。这种机制就叫做“多态”。例 CBase * p = &ODerived; p -> SomeVirtualFunction();
15
程序设计实习2007 派生类的对象可以赋给基类引用。 通过该基类引用调用基类和派生类中的同名虚函数时,若该引用引用的是一个基类的对象,那么被调用是基类的虚函数,如果引用的是一个派生类的对象,那么被调用的是派生类的虚函数。这种机制也叫 “多态”。例: CBase & r = ODerived; r.SomeVirtualFunction(); “多态”就是指上述这两种机制
16
三、 多态的例子 class A { public :
程序设计实习2007 三、 多态的例子 class A { public : virtual void Print( ) { cout << “A::Print”<<endl ; } }; class B:public A { virtual void Print( ) { cout << “B::Print” <<endl; } class D: public A { public: virtual void Print( ) { cout << “D::Print” << endl ; } class E: public B { virtual void Print( ) { cout << “E::Print” << endl ; }
17
A * pa; B * pb; D * pd ; E * pe; A a; B b; E e; D d;
程序设计实习2007 main() { A * pa; B * pb; D * pd ; E * pe; A a; B b; E e; D d; pb = & b; pd = & d; pe = & e; pa = & a; pa->Print(); // a.Print()被调用,输出:A::Print pa = pb; // 派生类指针赋值给基类指针 pa -> Print(); // b.Print()被调用,输出:B::Print pa = pd; pa -> Print(); // d. Print ()被调用,输出:D::Print pa = pe; pa -> Print() ; // e.Print () 被调用,输出:E::Print }
18
程序设计实习2007 四、 动态联编: 一条函数调用语句在编译时无法确定调用哪个函数,运行到该语句时才确定调用哪个函数,这种机制叫动态联编。
19
class A {public: virtual void Get(); };
程序设计实习2007 多态是通过动态联编实现的: class A {public: virtual void Get(); }; class B : public A { public: virtual void Get(); }; void MyFunction( A * pa ) { pa->Get(); } pa->Get() 调用的是 A::Get()还是B::Get(),编译时无法确定,因为不知道MyFunction被调用时,形参会对应于一个 A 对象还是B对象。 所以只能等程序运行到 pa->Get()了,才能决定到底调用哪个Get()
20
五、 普通函数调用虚函数实现多态 class Base { public: void fun1() { fun2(); }
程序设计实习2007 五、 普通函数调用虚函数实现多态 class Base { public: void fun1() { fun2(); } virtual void fun2() { cout << "Base::fun2()" << endl; } }; class Derived:public Base { virtual void fun2() { cout << "Derived:fun2()" << endl; } main() { Derived d; Base * pBase = & d; pBase -> fun1(); // 输出: Derived:fun2()
21
调用的次序是:Base::fun1() -> Derived::fun2(); 因为 void fun1() { fun2(); }
程序设计实习2007 调用的次序是:Base::fun1() -> Derived::fun2(); 因为 void fun1() { fun2(); } 相当于 this->fun2(); 编译这个函数的代码的时候,由于fun2()是虚函数,this是基类指针,所以是动态联编 上面这个程序运行到fun1函数中时, this指针指向的是d ,所以经过动态联编,调用的是Derived::fun2()
22
六. 纯虚函数和抽象类 纯虚函数: 没有函数体的虚函数 class A { private: int a; public:
程序设计实习2007 六. 纯虚函数和抽象类 纯虚函数: 没有函数体的虚函数 class A { private: int a; public: virtual void Print( ) =0 ; //纯虚函数 void fun() { cout << “fun”; } }; 包含纯虚函数的类叫抽象类 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象 抽象类的指针和引用可以指向由抽象类派生出来的类的对象 A a ; // 错,A 是抽象类,不能创建对象 A * pa ; // ok,可以定义抽象类的指针和引用 pa = new A ; //错误, A 是抽象类,不能创建对象
23
前面提出问题的解 问题是如何将输入的不同图形按顺序存储并输出
程序设计实习2007 前面提出问题的解 问题是如何将输入的不同图形按顺序存储并输出 解决方案:定义矩形和圆形的父类CShape, 将求面积,求周长,输出面积、周长和参数的函数定义为CShape中的纯虚函数,分别在矩形和圆形的类中给出函数的实现; 将输入的对象指针依次插入CShape 类的对象指针数组,通过多态实现对这些图形的遍历输出。
24
virtual int calcArea()=0; virtual int calcLength()=0;
程序设计实习2007 class CShape{ public: virtual int calcArea()=0; virtual int calcLength()=0; virtual void print()=0; };
25
class CRectangle:public CShape { private: int x1,y1,x2,y2; public :
程序设计实习2007 class CRectangle:public CShape { private: int x1,y1,x2,y2; public : friend istream& operator >> (istream & input, CRectangle & rect); int calcArea(){ return (x2-x1)*(y2-y1);} int calcLength(){ return 2*(x2-x1+y2-y1);} void print(){ cout << "面积 = " << calcArea() << ", 周长 = " << calcLength() << " " << " 左上角 = (" << x1 << "," << y1 << ")" << " 右下角 = (" << x2 << "," << y2 << ")" << endl;} };
26
istream& operator >> (istream & input, CRectangle & rect){
程序设计实习2007 istream& operator >> (istream & input, CRectangle & rect){ rect.x1 = rect.y1 = rect.x2 = rect.y2 = -1; while(rect.x1==-1){ cout <<"\n请输入左上角(x1,y1)和右下角坐标(x2,y2)" << endl; cout <<"要求x1,y1,x2,y2 都是非负整数并且x1<x2,y1<y2" << endl; cout << "例如:输入 表示 左上角(5,6)右下角(7,8)" << endl; input >> rect.x1 >> rect.y1 >> rect.x2 >> rect.y2; if(rect.x1<0 || rect.y1<0 || rect.x2<0 || rect.y2<0 || rect.x1>=rect.x2 || rect.y1>=rect.y2){ continue; } return input;
27
class CCircle:public CShape { private: int centerx ,centery, radius;
程序设计实习2007 class CCircle:public CShape { private: int centerx ,centery, radius; public : friend istream& operator >> (istream & input, CCircle & cir); int calcArea(){ return (int)(radius*radius* );} int calcLength(){ return (int)(2*radius* );} void print(){ cout << "面积 = " << calcArea() << ", 周长 = " << calcLength() << " " << "圆心 = (" << centerx << "," << centery << ")" << " 半径 =" << radius << endl; } };
28
istream& operator >> (istream & input, CCircle & cir){
程序设计实习2007 istream& operator >> (istream & input, CCircle & cir){ cir.centerx = cir.centery = cir.radius = -1; while(cir.centerx==-1){ cout <<"\n请输入圆心(x1,y1)和半径 r" << endl; cout <<"要求x1,y1,r 都是非负整数" << endl; cout << "例如:输入5 6 7 表示圆心(5,6)半径是 7 " << endl; cin >> cir.centerx >> cir.centery >> cir.radius ; if(cir.centerx<0 || cir.centery<0 || cir.radius<0){ continue; } return input;
29
CShape *shapes[100]; int nShapes; nShapes = 0; char choice;
程序设计实习2007 void main(){ CShape *shapes[100]; int nShapes; nShapes = 0; char choice; cout << "请选择图形:(R - Rectangle; C - Circle; E - 结束)" << endl; cin >> choice; while(choice !='E' && choice !='e'){ if(choice == 'R' || choice=='r'){ CRectangle *test1 = new CRectangle; cin >> *test1; test1 -> print(); shapes[nShapes++]=test1; }else if(choice == ‘C' || choice==‘c'){ CCircle *test2 = new CCircle;; cin >> *test2; test2 -> print(); shapes[nShapes++]=test2; } cout << "请选择:(R - Rectangle; C - Circle; E - 结束)" << endl; }// while for(int i=0;i<nShapes;i++){ shapes[i]->print(); delete shapes[i]; }
30
程序设计实习2007 多态的实质 将几个有类似行为的类的共同部分抽象成一个共同父类,并把这些类似行为的接口定义在父类中,把他们的具体实现定义在子类中,这样就可以通过父类以相同的方式操作不同子类的对象. 多态的另一个好处是能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少
31
程序设计实习2007 多态增强程序可扩充性的例子 游戏《魔法门之英雄无敌》
32
游戏中有很多种怪物,每种怪物都有一个类与之对应,每个怪物就是一个对象
程序设计实习2007 类:CSoldier 类:CAngel 类:Dragon 类CPhonex 游戏中有很多种怪物,每种怪物都有一个类与之对应,每个怪物就是一个对象
33
怪物能够互相攻击,攻击敌人和被攻击时都有相应的动作,动作是通过对象的成员函数实现的
程序设计实习2007 怪物能够互相攻击,攻击敌人和被攻击时都有相应的动作,动作是通过对象的成员函数实现的
34
游戏版本升级时,要增加新的怪物--雷鸟。如何编程才能使升级时的代码改动和增加量较小?
程序设计实习2007 游戏版本升级时,要增加新的怪物--雷鸟。如何编程才能使升级时的代码改动和增加量较小? 新增类:CThunderBird
35
为每个怪物类编写 Attack、FightBack和 Hurted成员函数
不论是否用多态编程,基本思路都是: 为每个怪物类编写 Attack、FightBack和 Hurted成员函数 Attack函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的 Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的 FightBack成员函数,遭受被攻击怪物反击 Hurted函数减少自身生命值,并表现受伤动作, FightBack成员函数表现反击动作,并调用被反击对象的Hurted成员函数,使被反击对象受伤 程序设计实习2007
36
int nLifeValue ; //代表生命值 public: int Attack( CWolf * pWolf) {
程序设计实习2007 1 非多态的实现方法 class CDragon { private: int nPower ; /代表攻击力 int nLifeValue ; //代表生命值 public: int Attack( CWolf * pWolf) { ...表现攻击动作的代码 pWolf->Hurted( nPower); pWolf->FightBack( this); } int Attack( CGhost * pGhost) { pGhost->Hurted( nPower); pGohst->FightBack( this);
37
int Hurted ( int nPower) { ....表现受伤动作的代码 nLifeValue -= nPower; }
int FightBack( CWolf * pWolf) { ....表现反击动作的代码 pWolf ->Hurt( nPower / 2); int FightBack( CGhost * pGhost) { pGhost->Hurt( nPower / 2 ); 有n种怪物,CDragon 类中就会有n个 Attack 成员函数,以及 n个FightBack 成员函数,对于其他类,比如CWolf等,也是这样 程序设计实习2007
38
以上为非多态的实现方法。如果游戏版本升级,增加了新的怪物雷鸟 CThunderBird, 则程序改动较大, 所有的类都需要增加:
程序设计实习2007 以上为非多态的实现方法。如果游戏版本升级,增加了新的怪物雷鸟 CThunderBird, 则程序改动较大, 所有的类都需要增加: int Attack( CThunderBird * pThunderBird) ; int FightBack( CThunderBird * pThunderBird) ; 成员函数,在怪物种类多的时候,工作量较大
39
2使用多态的实现方法: 设置基类 CCreature,并且使CDragon,CWolf等其他类都从CCreature派生而来
程序设计实习2007 2使用多态的实现方法: 设置基类 CCreature,并且使CDragon,CWolf等其他类都从CCreature派生而来 CCreature CDragon CWolf CSoldier CPhonex CAngel
40
int nLifeValue, nPower; public:
程序设计实习2007 基类 CCreature: class CCreature { private : int nLifeValue, nPower; public: virtual void Attack( CCreature * pCreature) = 0; virtual int Hurted( int nPower) = 0; virtual int FightBack( CCreature * pCreature) = 0; } 基类只有一个 Attack 成员函数 也只有一个 FightBack成员函数 所有CCreature 的派生类也是这样
41
class CDragon : public CCreature { public:
virtual void Attack( CCreature * pCreature) { pCreature ->Hurted( nPower); pCreature ->FightBack( this); } virtual int Hurted( int nPower) { … }; virtual int FightBack( CCreature * pCreature) { …}; 那么当增加新怪物雷鸟的时候,只需要编写新类CThunderBird, 不需要在已有的类里专门为新怪物增加 int Attack( CThunderBird * pThunderBird) ; int FightBack( CThunderBird * pThunderBird) ; 成员函数 程序设计实习2007
42
Dragon.Attack( & Wolf); //(1) Dragon.Attack( & Ghost); //(2)
程序设计实习2007 具体使用这些类的代码: 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
43
程序设计实习2007 作业 阅读第10章 几何形体练习2 在几何形体练习1的基础上,对用户输入的全部几何形体按照面积从小到大进行排序,并输出排序的结果。具体要求见作业描述,重点体会多态的应用,思考如何排序。
Similar presentations