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

    • 没有函数体的虚函数即为纯虚函数,包含纯虚函数的类叫抽象类。
    • 抽象类只能作为基类来派生新类使用,不能创建独立的抽象类的对象,但抽象类的指针和引用可以指向由抽象类派生出来的类的对象。
    • 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部不能调用纯虚函数。
    • 如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。
  • 相关阅读:
    electron 持续获取程序运行时间
    nfc reader iso7816 读卡器参考资料
    vue项目
    【数学】充分必要条件
    .Net 【DevExpress】 GridControl常用功能
    .Net 【基础回顾】关键字补充default
    .Net 【工作应用】 Ado.net总结
    .Net 【DevExpress】 MemoEdit总结
    发送短信封装
    Codeforces 1666 Labyrinth
  • 原文地址:https://www.cnblogs.com/inchbyinch/p/12398403.html
Copyright © 2020-2023  润新知