23 多态与虚函数

面向对象程序设计有三大特征:封装性、继承性、多态性。

三大特征是互相关联的,多态性必须存在于继承的环境中,是继承性的进一步拓展。

1 多态要解决的问题

让我们稍稍修改下上一节中的代码:

#include<iostream>

using namespace std;

//基类 People
class People {
protected:
    string m_name;
    int m_age;
public:
    People(string name, int age) : m_name(name), m_age(age) {}
    // 基类中的 show() 函数
    void show(){
        cout<<"嗨,大家好,我叫"<<m_name<<",今年"<<m_age<<"岁。"<<endl;
    }
};

//派生类 Student
class Student : public People {
private:
    float m_score;
public:
    Student(string name, int age, float score) : People(name, age), m_score(score) {}
    // 派生类中的 show() 函数
    void show() {
        cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << "。" << endl;
    }
};

int main() {
    Student stu("小明", 16, 90.5);
    stu.show();
    People& p1 = stu;
    p1.show();

    return 0;
}

程序输出:
小明的年龄是16,成绩是90.5。
嗨,大家好,我叫小明,今年16岁。

从输出结果可以看出:使用派生类对象名 stu 的语句 stu.show(); 调用的是派生类中的 show() 函数,而 使用基类引用 p1 的语句 p1.show(); 调用的是基类中的 show() 函数。

简单地说,我们面临的问题是:通过基类指针只能访问基类的成员函数,但是不能访问派生类的成员函数。这导致我们无法通过父类指针调用子类重写过的函数。

2 虚函数

让基类指针能够访问派生类的成员函数,C++ 增加了虚函数(Virtual Function)。使用虚函数非常简单,只需要在函数声明前面增加 virtual 关键字。

让我们将上面的基类中的 show() 函数转变为虚函数:

#include<iostream>

using namespace std;

//基类 People
class People {
protected:
    string m_name;
    int m_age;
public:
    People(string name, int age) : m_name(name), m_age(age) {}
    // 现在,将基类的 show() 函数设为虚函数
    virtual void show(){
        cout<<"嗨,大家好,我叫"<<m_name<<",今年"<<m_age<<"岁。"<<endl;
    }
};

//派生类 Student
class Student : public People {
private:
    float m_score;
public:
    Student(string name, int age, float score) : People(name, age), m_score(score) {}

    void show() {
        cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << "。" << endl;
    }
};

int main() {
    Student stu("小明", 16, 90.5);
    stu.show();
    People& p1 = stu;
    p1.show();

    return 0;
}

程序输出:
小明的年龄是16,成绩是90.5。
小明的年龄是16,成绩是90.5。

和前面的例子相比,本例仅仅是在 show() 函数声明前加了一个 virtual 关键字,将成员函数声明为了虚函数。这样就可以通过 p1 指针调用 Student 类的成员函数了,运行结果也证明了这一点。

特别说明:

  1. 静态函数和构造函数不能是虚函数,析构函数可以是虚函数。如果不使用虚析构函数,在使用 delete 基类指针 时,将不会调用派生类的析构函数。
  2. 可以通过指针或引用实现对虚函数的调用。但因为引用不可以改变其指向的对象的特点,使其不够灵活,所以在调用虚函数方面,指针更常用。

3 纯虚函数与抽象类

有时在基类中,将某函数设置为虚函数,并不是因为基类本身很需要这个函数,而是考虑到派生类的需要,才在基类中留下一个函数名,具体功能交给派生类实现。

这时,我们就不必为基类中的虚函数的函数体写任何内容,甚至我们可以不写代表函数体的大括号,只写个 = 0,这种虚函数叫做 “纯虚函数” ,语法如下:

virtual 返回值类型 函数名(参数列表) = 0;

注意:拥有纯虚函数的类不可以用于定义对象,但可以定义其指针或引用。

抽象类:拥有纯虚函数的类被称为 “抽象类”。

注意:抽象类的派生类,如果没有实现所有的抽象类中的纯虚函数,那此派生类仍为抽象类。

4 多态的用途

通过上面的例子,大家可能还未发现多态的用途,不过确实也是,多态在小项目中鲜有有用武之地。

接下来的例子中,我们假设你正在玩一款军事游戏:玩家进入了几名敌人组成的包围圈,敌人包括弓箭手和剑客,敌人对玩家展开了进攻。

现在,让我们用代码实现这个过程:

#include<iostream>

using namespace std;

// 玩家类
class Player {
private:
    // 生命值
    int HP;
public:
    Player(int hp) : HP(hp) {}

    // 玩家受到攻击时调用,传入的参数为敌人的攻击力,玩家会掉血,掉到0会死
    void getDamage(int damage) {
        HP -= damage;
        if (HP <= 0) {
            cout << "啊我死了!" << endl;
        } else {
            cout << "好疼!" << endl;
        }
    }
};

// 敌人基类 Enemy
class Enemy {
protected:
    // 敌人的基础攻击力
    int _damage;
public:
    Enemy(int damage) : _damage(damage) {}

    virtual ~Enemy() {};

    // 敌人对玩家发动攻击的行为
    virtual void attack(Player *player) = 0;
};

// 弓箭手类 Archer
class Archer : public Enemy {
private:
    // 弓箭手的箭的攻击力
    int _damageOfArrow;
public:
    Archer(int damage, int damageOfArrow) : Enemy(damage), _damageOfArrow(damageOfArrow) {}

    // 弓箭手对玩家发动攻击,射出一箭。传入参数为受攻击的对象。
    void attack(Player *player) {
        cout << "射一箭!" << endl;
        player->getDamage(_damage + _damageOfArrow);
    }
};

// 剑客类 Swordsman
class Swordsman : public Enemy {
private:
    // 剑客的剑的攻击力
    int _damageOfSword;
public:
    Swordsman(int damage, int damageOfSword) : Enemy(damage), _damageOfSword(damageOfSword) {}

    // 剑客对玩家发动攻击,砍一刀。传入参数为受攻击的对象。
    void attack(Player *player) {
        cout << "砍一刀!" << endl;
        player->getDamage(_damage + _damageOfSword);
    }
};

int main() {
    // 创建 player 对象,初始生命值为 100
    Player *player = new Player(100);
    // 创建包围玩家的敌人对象列表
    Enemy *enemys[3] = {new Swordsman(10, 30), new Archer(15, 20), new Archer(15, 20)};
    for (Enemy *e: enemys) {
        e->attack(player);
    }
    delete player;
    for (Enemy *e: enemys) {
        delete e;
    }
    return 0;
}

程序输出:
砍一刀!
好疼!
射一箭!
好疼!
射一箭!
啊我死了!

从这个案例我们可以看出,正因为有了多态的存在,我们才可以简单地使用 “敌人类” 的指针,通过 for 循环控制所有敌方对象对玩家展开进攻,否则,我们将不得不为不同的 “敌人类” 的派生类写大量几乎完全相同的代码,单独控制它们对玩家发起攻击。

因为虚函数需要在运行时,才可以知道究竟要运行哪一个派生类的实现,所以运行效率较低。在实际开发中,需要根据需求判断是否使用虚函数。