虚函数和多态

本文是中国大学MOOC的郭炜老师网课的笔记,纯本人手打,如果觉得还行,不妨点个赞叭!

1. 虚函数和多态的基本概念

1.1 虚函数

在类的定义中,前面有virtual关键字的成员函数就是虚函数:

// 类的定义
class base{
    virtual int get();
};

// 函数的实现
int base::get()
{}

virtual 关键字只用在类定义里的函数声明中,写函数体时不用。

1.2 多态的表现形式

多态的表现形式有两种:

  1. 指针
  • 派生类的指针可以赋给基类指针。

  • 通过基类指针调用基类和派生类中的同名虚函数时:

    • 若该指针指向一个基类的对象,那么被调用是基类的虚函数;
    • 若该指针指向一个派生类的对象,那么被调用的是派生类的虚函数。

这种机制就叫做“多态”。

例如:

class CBase{
public:
    virtual void f(){}
};

class CDerived:public CBase{
public:
    virtual void f(){}
};

int main(){
    CDerived ODerived;
    CBase* p = & ODerived;
    p->f(); // 调用哪个虚函数取决于p指向哪种类型的对象
    return 0;
}
  1. 引用
  • 派生类的对象可以赋给基类引用
  • 通过基类引用调用基类和派生类中的同名虚函数时:
    • 若该引用引用的是一个基类的对象,那么被调用是基类的虚函数;
    • 若该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数

这种机制就叫做“多态”。

例如:

class CBase{
public:
    virtual void f(){}
};

class CDerived:public CBase{
public:
    virtual void f(){}
};

int main(){
    CDerived ODerived;
    CBase& r = ODerived;
    r.f(); // 调用哪个虚函数取决于r引用哪种类型的对象
    return 0;
}

1.3 多态的作用 ⭐️

在面向对象的程序设计中使用多态,能够增强程序的可扩充性,即程序需要修改或增加功能的时候,需要改动和增加的代码较少。

总而言之,使用虚函数是实现了一个接口多种方法。

2. 使用多态的游戏程序实例

游戏《魔法门之英雄无敌》

游戏中有若干种怪物,每种怪物都有一个类与之对应,每个怪物就是一个对象,例如有CSoldier、CDragon、CPhonex等怪物;

怪物可以互相攻击,攻击敌人和被攻击时都会有相应的动作,动作是通过对象的成员函数实现的;

现在游戏升级,要增加新的怪物–CThunderBird。

那么问题来了,如何编程能使升级时代码的改动和增加量较小?

2.1 原始方法

每个怪物类编写Attack、FightBack和 Hurted成员函数:

  • Attact函数表现攻击动作,攻击某个怪物,并调用被攻击怪物的Hurted函数,以减少被攻击怪物的生命值,同时也调用被攻击怪物的FightBack成员函数,遭受被攻击怪物反击。

  • Hurted函数减少自身生命值,并表现受伤动作。

  • FightBack成员函数表现反击动作,并调用被反击对象的Hurted成员函数,使被反击对象受伤。

然后设置基类CCreature,并且使CDragon、CWolf等其它类都从该类派生而来;

这样的话,有n种怪物,CDragon类中就有n个Attack成员函数,以及n个FightBack成员函数,对于其它类也是如此。

如果新增其它类,那么所有的类都需要增加两个成员函数

void Attack( CThunderBird * pThunderBird);
void FightBack( CThunderBird * pThunderBird) ;

在怪物种类较多时,工作量很大。

2.2 使用多态的改进方法

思路:

基类只有一个Attack成员函数,也只有一个FightBack成员函数,所有CCreature的派生类也是这样。

假如现在CDragon要攻击CPhonex,那么将调用CDragon::Attack(&CPhonex);函数

上述函数的实现形式是这样的:

CDragon::Attack(CCreature* p){
    // 攻击的动作
    p->Hurted(m_nPower); // 多态
    p->FightBack(this); // 多态
}

基类的指针p指向CPhonex这一派生类,根据多态,p->Hurted()调用的是CPhonex的Hurted()函数

CPhonex的受伤函数如下:

CPhonex::Hurted(int nPower){
    // 受伤的动作
    m_nLifeValue -= nPower;
}

接着,p->FightBack(this)调用的是CPhonex的FightBack()函数

成员函数通过一个名为this的隐式额外参数来访问调用它的对象。this参数是一个常量指针,被初始化为调用该函数的对象地址。在函数体内可以显式使用this指针。

默认情况下,this的类型是指向类类型非常量版本的常量指针。

CPhonex的反击函数的实现形式如下:

CPhonex::FightBack(CCreature* p){
    // 反击的动作
    p->Hurted(m_nPower/2); // 多态
}

由前面可以知道,this传入的是调用CDragon::Attack的对象地址,那么此时基类的指针p指向CDragon这一派生类,根据多态,p->Hurted()调用的是CDragon的Hurted()函数。

CCreature.h 类及函数的声明

// CCreature.h
#include <iostream>

class CCreature{
protected:
    int nPower;
    int nLifeValue;
    std::string m_Name;
public:
    virtual std::string My_Name() = 0;
    virtual void Attack(CCreature* pCreature){}
    virtual void Hurted(int nPower){}
    virtual void FightBack(CCreature* pCreature){}
};

class CDragon : public CCreature{ // 龙
protected:
    int m_nPower = 50;
    int m_nLifeValue = 180;
    std::string m_Name;
public:
    CDragon(std::string name);
    std::string My_Name();
    void print_name();
    void Attack(CCreature* pCreature);
    void Hurted(int nPower);
    void FightBack(CCreature* pCreature);
};

class CPhonex : public CCreature{ // 凤凰
protected:
    int m_nPower = 60;
    int m_nLifeValue = 160;
    std::string m_Name;
public:
    CPhonex(std::string str) {m_Name = str;}
    std::string My_Name();
    void Attack(CCreature* pCreature);
    void Hurted(int nPower);
    void FightBack(CCreature* pCreature);
};

class CThunderBird : public CCreature{ // 雷鸟
protected:
    int m_nPower = 30;
    int m_nLifeValue = 120;
    std::string m_Name;
public:
    CThunderBird(std::string str) {m_Name = str;}
    std::string My_Name();
    void Attack(CCreature* pCreature);
    void Hurted(int nPower);
    void FightBack(CCreature* pCreature);
};

CCreature.cpp 函数的实现

// CCreature.cpp
#include "CCreature.h"

using namespace std;

// 构造函数实现
CDragon::CDragon(std::string name){
    cout << "CDragon has been created: " << name << endl;
    m_Name = name;
    cout << "m_Name = " << m_Name << endl;
}

// Dragon虚函数的实现
void CDragon::Attack(CCreature* p){
    printf("Now, %s Attacking %s.\n", m_Name.c_str(), p->My_Name().c_str());
    p->Hurted(m_nPower);
    p->FightBack(this);
}
void CDragon::Hurted(int nPower){
    printf("Now, %s has been hurted.\n", m_Name.c_str());
    m_nLifeValue -= nPower;
    printf("Now, %s's LifeValue is %d.\n", m_Name.c_str(), m_nLifeValue);
}
void CDragon::FightBack(CCreature* p){
    printf("Now, %s fightback.\n", m_Name.c_str());
    p->Hurted(m_nPower/2);
}

void CDragon::print_name(){
    printf("my name is %s.\n", m_Name.c_str());
}

// Phonex虚函数的实现
void CPhonex::Attack(CCreature* p){
    printf("Now, %s Attacking.\n", m_Name.c_str());
    p->Hurted(m_nPower);
    p->FightBack(this);
}
void CPhonex::Hurted(int nPower){
    printf("Now, %s has been hurted.\n", m_Name.c_str());
    m_nLifeValue -= nPower;
    printf("Now, %s's LifeValue is %d.\n", m_Name.c_str(), m_nLifeValue);
}
void CPhonex::FightBack(CCreature* p){
    printf("Now, %s fightback.\n", m_Name.c_str());
    p->Hurted(m_nPower/2);
}

// ThunderBird虚函数的实现
void CThunderBird::Attack(CCreature* p){
    printf("Now, %s Attacking.\n", m_Name.c_str());
    p->Hurted(m_nPower);
    p->FightBack(this);
}
void CThunderBird::Hurted(int nPower){
    printf("Now, %s has been hurted.\n", m_Name.c_str());
    m_nLifeValue -= nPower;
    printf("Now, %s's LifeValue is %d.\n", m_Name.c_str(), m_nLifeValue);
}
void CThunderBird::FightBack(CCreature* p){
    printf("Now, %s fightback.\n", m_Name.c_str());
    p->Hurted(m_nPower/2);
}

std::string CDragon::My_Name(){
    return m_Name;
}

std::string CPhonex::My_Name(){
    return m_Name;
}

std::string CThunderBird::My_Name(){
    return m_Name;
}

主函数

// main.cpp

#include "CCreature.h"

using namespace std;

// 基类只有一个Attack成员函数,也只有一个FightBack成员函数
// 所有CCreature的派生类也是这样

int main(){
    CDragon D1("dragon_1"), D2("dragon_2");
    D1.print_name();
    D2.print_name();
    CPhonex P1("phonex_1");
    CThunderBird T1("thunderbrid_1");
    D1.Attack(&P1);
    D1.Attack(&T1);
    D1.Attack(&D2);
    return 0;
}

3. 多态实例:几何形体程序

问题

几何形体处理程序:输入若干个几何形体的参数,要求按面积排序输出。输出时要指明形状。

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

思路

  1. 如何存放每个几何形体

用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能按面积从小到大依次输出每个几何体的种类及其面积。

  1. 如何从小到大排列

用sort,建立cmp()函数,利用基类指针指向派生类对象的Area()来比较。

代码

#include <iostream>
#include <algorithm>
#include <vector>
#include <math.h>

using namespace std;

class CShape{
public:
    virtual double Area() = 0; // 纯虚函数
    virtual void PrintInfo() = 0;
};

class CTriangle : public CShape{
public:
    CTriangle(double x, double y, double z) {a = x, b = y, c = z;}
    double Area(){
        double p = (a + b + c) / 2.0;
        return sqrt(p * (p-a) * (p-b) * (p-c));
    }
    void PrintInfo(){
        cout << "Triangle's Area is " << Area() << endl;
    }
private:
    double a, b, c;
};

class CCricle : public CShape{
public:
    CCricle(double a) {r = a;}
    double Area(){
        return 3.14 * r * r;
    }
    void PrintInfo(){
        cout << "Cricle's Area is " << Area() << endl;
    }
private:
    double r;
};

class CRectangle : public CShape{
public:
    CRectangle(double a, double b) {width = a, hight = b;}
    double Area(){
        return width * hight;
    }
    void PrintInfo(){
        cout << "rectangle's Area is " << Area() << endl;
    }
private:
    double width, hight;
};

// CShape* pShapes[100]; // 用基类指针数组存放指向各种派生类对象的指针,然后遍历该数组,就能对各个派生类对象做各种操作,是很常见的做法
vector<CShape*> vec;
bool cmp(CShape* s1, CShape* s2){
    return s1->Area() < s2->Area(); // 此句为多态,s1的类型是CShape*,是基类指针
}

int main(){
    int n;
    cin >> n;
    CRectangle* pr; 
    CCricle* pc;
    CTriangle* pt;
    for (int i = 0; i < n; i++){
        char c; 
        cin >> c;
        switch(c){
            case 'R':
                double width, hight;
                cin >> width >> hight;
                pr = new CRectangle(width, hight);
                // pShapes[i] = pr;
                vec.push_back(pr);
                break;
            case 'T':
                double a, b, c;
                cin >> a >> b >> c;
                pt = new CTriangle(a, b, c);
                // pShapes[i] = pt;
                vec.push_back(pt);
                break;
            case 'C':
                double r;
                cin >> r;
                pc = new CCricle(r);
                // pShapes[i] = pc;
                vec.push_back(pc);
                break;
        }
    }
    // sort(pShapes, pShapes + n, cmp);
    // for (int i = 0; i < n; i++){
    //     vec.push_back(pShapes[i]);
    //     pShapes[i]->PrintInfo();
    // }

    sort(vec.begin(), vec.end(), cmp);
    for (int i = 0; i < n; i++){
        vec[i]->PrintInfo();
    }
    return 0;
}

PS:

在构造函数和析构函数中调用虚函数,不是多态。编译时即可确定,调用的函数是自己的类或基类中定义的函数,不会等到运行时才决定调用自己的还是派生的函数。

指向基类的指针可以指向派生类对象,当基类指针指向派生类对象时,这种指针只能访问派生对象从基类继承而来的那些成员,不能访问子类特有的元素;

例如:有基类B和从B派生的子类D,则B *p; D dd; p = &dd;

此时指针p只能访问从基类派生而来的成员,不能访问派生类D特有的成员,因为基类不知道派生类中的这些成员。

4. 多态的实现原理

多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定。

—这一过程叫“动态联编”

动态联编如何实现的呢?

虚函数表

每一个有虚函数的类(或有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着虚函数表的指针。

虚函数表中列出了该类的虚函数表地址,多出来的4个字节就是用来放虚函数表的地址的。

5. 虚析构函数、纯虚函数和抽象类

5.1 虚析构函数

通过基类的指针删除派生类对象时,通常情况下只调用基类的析构函数,但是,删除一个派生类的对象时,应该先调用派生类的析构函数,然后调用基类的析构函数。

解决方法:把基类的析构函数声明为virtual
- 派生类的析构函数可以virtual不进行声明
- 通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数

一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义为虚函数。

PS:不允许以虚函数作为构造函数

例子

#include <iostream>

using namespace std;

class Base{
    public:
        int i;
        virtual void print(){
            cout << "Base";
        }
        virtual ~Base(){cout << "Bye from Base!" << endl;};
        // 如果这里不加virtual关键字,那么通过基类的指针删除派生类对象时,只调用基类的析构函数
};

class Derived : public Base{
    public:
        int n;
        virtual void print(){
            cout << "Derived";
        }
        virtual ~Derived(){cout << "Bye from Derived!" << endl;};
};

int main(){
    Base* pb;
    pb = new Derived();
    delete pb;
    return 0;
}

结果:

Bye from Derived!
Bye from Base!

虚析构函数解决了通过基类指针去delete派生类对象的问题

5.2 纯虚函数和抽象类

纯虚函数:没有函数体的虚函数

抽象类:包含纯虚函数的类

  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象

  • 抽象类的指针和引用可以指向由抽象类派生出来的类的对象

假设A是抽象类:

A a; // 错,A是抽象类,不可以创建对象
A *pa; // ok,可以定义抽象类的指针和引用
pa = new A; // 错误,A是抽象类,不能创建对象

如果一个类从抽象类派生而来,那么当且仅当它实现了基类的所有纯虚函数,它才能成为非抽象类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值