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
类的成员函数了,运行结果也证明了这一点。
特别说明:
- 静态函数和构造函数不能是虚函数,析构函数可以是虚函数。如果不使用虚析构函数,在使用
delete 基类指针
时,将不会调用派生类的析构函数。- 可以通过指针或引用实现对虚函数的调用。但因为引用不可以改变其指向的对象的特点,使其不够灵活,所以在调用虚函数方面,指针更常用。
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
循环控制所有敌方对象对玩家展开进攻,否则,我们将不得不为不同的 “敌人类” 的派生类写大量几乎完全相同的代码,单独控制它们对玩家发起攻击。
因为虚函数需要在运行时,才可以知道究竟要运行哪一个派生类的实现,所以运行效率较低。在实际开发中,需要根据需求判断是否使用虚函数。