11.7 多态
11.7.1 多态的基本概念
1)多态分为两类:
- 静态多态:函数重载与运算符重载属于静态多态,复用函数名
- 动态多态:派生类和虚函数实现运行时多态
2)二者的区别:
- 静态多态:静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态:动态多态的函数地址晚绑定 - 运行阶段确定函数地址
3)动态多态的满足条件:
- 存在继承关系
- 子类中重写父类中的虚函数(父类中的虚函数即在正常函数定义前加virtual)
4)动态多态的使用:
- 父类的指针或引用 指向子类
5)多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期与后期的扩展与维护
示例:
class animal {
public:
virtual void speak() { // 虚函数
cout << "animal 在说话" << endl;
}
};
class cat :public animal { //存在继承关系
public:
void speak() { // 子类中重写父类中的虚函数
cout << "cat 在说话" << endl;
}
};
class dog :public animal { // 存在继承关系
public:
void speak() { // 子类中重写父类中的虚函数
cout << "dog 在说话" << endl;
}
};
void doSpeak(animal& animal) { // 父类的引用
animal.speak();
}
void test() {
cat cat1;
dog dog1;
animal animal1;
doSpeak(cat1); // 指向子类
doSpeak(dog1); // 指向子类
doSpeak(animal1);
}
11.7.2 多态的底层原理
基础知识:
- 虚函数(表)指针
vfptr:当类中出现虚函数时,会构建一个虚函数指针,指向虚函数表 - 虚函数表
vftable:表内记录虚函数的地址
以11.7.1节中的代码为例,使用Developer Command Prompt查看类结构分别如下:
-
当父类不使用虚函数时,代码如下
class animal { public: void speak() { cout << "animal 在说话" << endl; } };-
此时
animal类的结构为class animal size(1): +--- +---可以看出,因为
animal类中仅有一个非静态成员函数,由《11.3.1 成员变量与成员函数分开储存》可知,成员函数不属于类实例化对象中的一部分,故此时animal为空类,只占1字节(负责占位)。
-
-
当父类使用虚函数,且子函数不进行虚函数重写时,代码如下
class animal { public: virtual void speak() { cout << "animal 在说话" << endl; } }; class cat :public animal { };-
此时
animal类结构为class animal size(4): +--- 0 | {vfptr} +--- animal::$vftable@: | &animal_meta | 0 0 | &animal::speak可以看出,此时
animal类占4字节,为4字节的虚函数指针vfptr,该指针指向animal类的虚函数表vftable,虚函数表中存储着虚函数speak()的地址&animal::speak。 -
此时
cat类结构为class cat size(4): +--- 0 | +--- (base class animal) 0 | | {vfptr} | +--- +--- cat::$vftable@: | &cat_meta | 0 0 | &animal::speak可以看出,
cat类由于继承了animal类,故也仅包含一个与父类一致的vfptr,指向父类虚函数的地址&animal::speak。
-
-
当父类使用虚函数,且子函数进行虚函数重写时,代码如下
class animal { public: virtual void speak() { cout << "animal 在说话" << endl; } }; class cat :public animal { public: void speak() { cout << "cat 在说话" << endl; } };-
此时
cat类结构为class cat size(4): +--- 0 | +--- (base class animal) 0 | | {vfptr} | +--- +--- cat::$vftable@: | &cat_meta | 0 0 | &cat::speak可以看出由于子类对父类的虚函数进行重写,虚函数表中原来的父类虚函数地址
&animal::speak被覆盖为子类重写函数的地址&cat::speak,此时cat类中的vfptr指向的是cat类重写的函数的地址。
-
综上:当父类定义了虚函数virtual void speak(),子类中进行重写后void speak(),通过父类的指针或引用指向子类时void doSpeak(animal& animal) doSpeak(cat1),实质上发生了animal& animal = cat1,doSpeak()函数会从cat1对象的虚函数表中寻找对应的函数地址进入,输出流cat在说话,产生了多态。简单来说:
- 父类定义虚函数:产生虚函数指针和虚函数表,且指针指向
animal类表中地址 - 子类发生继承:继承了虚函数指针和虚函数表,且指针指向
animal类表中地址 - 子类进行重写:将指针指向
cat类表中地址 - 父类的指针或引用指向子类:从子类
cat的表中寻找函数地址,运行对应函数
11.7.3 (案例)计算器类的实现
class AbstractCalculator { // 构建抽象的计算器类
public:
virtual int getResult() { // 无需进行计算操作
return 0;
}
int m_num1, m_num2;
};
class AddCalculator:public AbstractCalculator { // 加法计算器类继承抽象计算器类
public:
int getResult() {
return m_num1+m_num2;
}
};
class SubCalculator :public AbstractCalculator { // 减法计算器类继承抽象计算器类
public:
int getResult() {
return m_num1 - m_num2;
}
};
void test() {
AbstractCalculator* abs = new AddCalculator; // 父类的指针abs指向子类
abs->m_num1 = 10;
abs->m_num2 = 20;
cout << "结果为: " << abs->getResult() << endl;
delete abs; // new创建后一定要delete销毁
abs = new SubCalculator; // 父类的指针abs指向子类
abs->m_num1 = 10;
abs->m_num2 = 20;
cout << "结果为: " << abs->getResult() << endl;
delete abs; // new创建后一定要delete销毁
}
11.7.4 纯虚函数和抽象类
纯虚函数:
1)背景:在多态中,父类中虚函数的实现一般是无意义的,我们主要用的是子类重写的内容,因此可以将虚函数改为纯虚函数。
2)语法:virtual 返回类型 函数名(参数列表) = 0
抽象类
1)定义:当类中有纯虚函数,则该类便是抽象类
2)特点:
- 无法实例化对象
- 子类必须重写抽象类中的纯虚函数,否则也为抽象类
11.7.5 (案例)制作饮品
案例描述:
制作饮品的步骤大致为:煮水 -> 冲泡 -> 倒入杯中 -> 加入辅料
利用多态技术实现:抽象制作饮品基类 和 制作茶水和咖啡子类
class AbstractDrinking {
public:
virtual void Boil() = 0; // 煮水
virtual void Brew() = 0; // 冲泡
virtual void PourInCup() = 0; // 倒入杯中
virtual void PutSomething() = 0; // 加入辅料
void doDrink() { // 制作流程
Boil();
Brew();
PourInCup();
PutSomething();
}
};
class makeCoffee :public AbstractDrinking {
public:
void Boil() {
cout << "烧开水" << endl;
}
void Brew() {
cout << "冲泡咖啡" << endl;
}
void PourInCup() {
cout << "倒入咖啡杯中" << endl;
}
void PutSomething() {
cout << "加入牛奶" << endl;
}
};
class makeTea :public AbstractDrinking {
public:
void Boil() {
cout << "烧开水" << endl;
}
void Brew() {
cout << "冲泡茶叶" << endl;
}
void PourInCup() {
cout << "倒入茶杯中" << endl;
}
void PutSomething() {
cout << "加入红枣" << endl;
}
};
void doWork(AbstractDrinking * abs) { // 传入父类指针
abs->doDrink(); // 制作某子类饮品
delete abs; // 销毁后面new出的对象(为什么?)
}
void test() {
doWork(new makeCoffee);
doWork(new makeTea);
}
11.7.6 虚析构和纯虚析构
1)背景:多态使用时,如果有子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
2)解决方式:将父类中的析构函数改为 虚析构 或 纯虚析构
3)虚析构与纯虚析构的共性:
- 可通过父类指针释放子类对象
- 都需要有具体的函数实现
4)虚析构与纯虚析构的区别:
- 若为纯虚析构,则该析构函数所属类为抽象类,无法实例化对象
5)语法:
- 虚析构:
virtual ~类名(){},即在正常析构函数前加virtual关键字 - 纯虚析构:需要 类内声明,类外实现
- 类内写:
virtual ~类名() = 0 - 类外写:
类名 :: ~类名(){}
- 类内写:
当不使用虚析构/纯虚析构时,有以下代码
class Animal {
public:
Animal() { // 父类构造函数
cout << "Animal构造函数调用" << endl;
}
virtual void speak() = 0; // 纯虚函数
~Animal() { // 父类析构函数
cout << "Animal析构函数调用" << endl;
}
};
class CAT :public Animal {
public:
CAT(string name) { // 子类构造函数
cout << "CAT构造函数调用" << endl;
m_name = new string(name);
}
void speak() { // 子类对父类中的纯虚函数进行重写
cout << *m_name << " speaking" << endl;
}
~CAT() { // 子类析构函数
cout << "CAT析构函数调用" << endl;
if (m_name != NULL) {
delete m_name;
m_name = NULL;
}
}
string* m_name;
};
void test() {
Animal* animal = new CAT("Tom"); // 子类对象创建到堆区,且父类指针指向子类对象
animal->speak();
delete animal; // 回收父类指针变量,只走父类的析构代码
}
/***************结果输出*******************/
//Animal构造函数调用
//CAT构造函数调用
//Tom speaking
//Animal析构函数调用
test代码流程:
new CAT("Tom")堆区开辟空间时,由11.6.4节可知,需要先调用父类构造函数(输出:Animal构造函数调用),再调用子类构造函数(输出:CAT构造函数调用),并将该堆区地址交由animal指针维护- 通过
animal指针调用speak()函数(输出:Tom speaking) - 回收
animal指针,由于animal属于Animal类,故调用的是Animal类的析构函数,但父类析构时不会调用子类的析构函数
可以看出Animal类正常构造+析构,但是CAT类只构造未析构,由于未执行CAT的析构代码,自然不会delete m_name,会造成内存泄漏。
**解决方法:**将基类Animal的析构函数改为虚析构或纯虚析构,即可正常输出如下
// Animal构造函数调用
// CAT构造函数调用
// Tom speaking
// CAT析构函数调用
// Animal析构函数调用
-
使用虚析构解决:
class Animal { public: Animal() { cout << "Animal构造函数调用" << endl; } virtual void speak() = 0; // 虚析构 virtual ~Animal() { cout << "Animal析构函数调用" << endl; } }; -
使用纯虚析构解决:
class Animal { public: Animal() { cout << "Animal构造函数调用" << endl; } virtual void speak() = 0; virtual ~Animal() = 0; }; Animal::~Animal() { cout << "Animal析构函数调用" << endl; }
11.7.7 (案例)组装计算机
需求:电脑由CPU、GPU、内存组成,、Inter和Lenovo两个厂商可制造上述三个部件,现在需要使用以上任意厂商的配件组装电脑
#include<iostream>
#include<string>
using namespace std;
// CPU 抽象类
class CPU {
public:
virtual void calculate() = 0;
};
// GPU 抽象类
class GPU {
public:
virtual void display() = 0;
};
// Memory 抽象类
class Memory {
public:
virtual void storage() = 0;
};
// Inter CPU 派生类
class InterCPU :public CPU {
public:
void calculate() {
cout << "启动Inter的CPU" << endl;
}
};
// Inter GPU 派生类
class InterGPU :public GPU {
public:
void display() {
cout << "启动Inter的GPU" << endl;
}
};
// Inter Memory 派生类
class InterMemory :public Memory {
public:
void storage() {
cout << "启动Inter的Memory" << endl;
}
};
// Lenovo CPU 派生类
class LenoveCPU :public CPU {
public:
void calculate() {
cout << "启动Lenove的CPU" << endl;
}
};
// Lenovo GPU 派生类
class LenoveGPU :public GPU {
public:
void display() {
cout << "启动Lenove的GPU" << endl;
}
};
// Lenovo Memory 派生类
class LenoveMemory :public Memory {
public:
void storage() {
cout << "启动Lenove的Memory" << endl;
}
};
// Computer操作类
class Computer {
public:
Computer(CPU* cpu, GPU* gpu, Memory* memory) {
m_cpu = cpu;
m_gpu = gpu;
m_memory = memory;
}
void doWork() {
m_cpu->calculate();
m_gpu->display();
m_memory->storage();
}
~Computer() { // 在Computer的析构函数中对传入的指针进行回收
cout << "Computer 析构" << endl;
if (m_cpu != NULL) {
delete m_cpu;
m_cpu = NULL;
}
if (m_gpu != NULL) {
delete m_gpu;
m_gpu = NULL;
}
if (m_memory != NULL) {
delete m_memory;
m_memory = NULL;
}
}
CPU* m_cpu;
GPU* m_gpu;
Memory* m_memory;
};
void test() {
// 第一台电脑
CPU* cpu = new InterCPU;
GPU* gpu = new InterGPU;
Memory* memory = new InterMemory;
cout << "第一台电脑" << endl;
Computer* pc1 = new Computer(cpu, gpu, memory);
pc1->doWork();
delete pc1;
// 第二台电脑
cout << "----------------------------------" << endl;
cout << "第二台电脑" << endl;
Computer* pc2 = new Computer(new LenoveCPU, new LenoveGPU, new LenoveMemory); // 上述的简化版,可直接在入口参数new
pc2->doWork();
delete pc2;
// 第三台电脑
cout << "----------------------------------" << endl;
cout << "第三台电脑" << endl;
Computer* pc3 = new Computer(new InterCPU, new LenoveGPU, new InterMemory);
pc3->doWork();
delete pc3;
}
int main() {
test();
system("pause");
return 0;
}
需要注意的是,传入Computer类的三个指针不一定非要在Computer的析构函数中回收,也可在test()的末尾进行回收。

被折叠的 条评论
为什么被折叠?



