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 纯虚函数和抽象类

  • 没有函数体的虚函数即为纯虚函数,包含纯虚函数的类叫抽象类。
  • 抽象类只能作为基类来派生新类使用,不能创建独立的抽象类的对象,但抽象类的指针和引用可以指向由抽象类派生出来的类的对象。
  • 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。
  • 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。
posted @ 2020-03-02 23:28  天地辽阔  阅读(716)  评论(0编辑  收藏  举报