文章目录
多态
1.多态基本概念
- 多态是c++面向对象三大特性,封装、继承、多态
- 多态可以分成两类:
- 静态多态:就是函数重载、操作符重载。多态就是多种形态,函数重载就是让相同函数名有多种形态表现出来,参数数量,类型不同。
- 动态多态:c++中多态一般指的是动态多态,派生类和虚函数实现运行时多态。
- 静态多态和动态多态的区别
- 静态多态的函数地址早绑定-编译阶段确定函数地址
- 动态多态的函数地址晚绑定-运行阶段确定函数地址
使用动态多态方法
- 父类的指针或引用,指向子类的对象
/*
创建一个动物类含有一个说话函数输出动物说话,
然后创建一个子类猫类也有一个说话函数输出小猫在说话。
创建一个执行说话的函数,函数参数为父类Animal,调用speak函数。
在test函数中创建猫类对象,然后对象传入dospeak()中,父类引用指向子类对象。
那输出到底是父类的speak还是子类的speak呢?
*/
#include "stdafx.h"
#include <iostream>
using namespace std;
//动物类
class Animal {
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
//猫类
class Cat : public Animal {
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
//执行说话的函数
void dospeak(Animal &animal) //Animal &animal=cat;父类的引用指向子类对象
{
animal.speak();
}
void test01()
{
Cat cat;
dospeak(cat);//传猫自然本意是想让猫说话
}
int main()
{
test01();
cout << "hello" << endl;
system("pause");
return 0;
}
/*但最后输出动物,是因为地址早在编译阶段就绑定了,
如果想执行让猫说话,这个函数的地址就要晚绑定,动态多态。
只需要在父类成员函数前添加virtual关键字即可
*/
添加狗类测试输入不同子类
#include "stdafx.h"
#include <iostream>
using namespace std;
//动物类
class Animal {
public:
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
//猫类
class Cat : public Animal {
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
//狗类
class Dog :public Animal {
public:
void speak()
{
cout << "狗在说话" << endl;
}
};
//执行说话的函数
void dospeak(Animal &animal) //Animal &animal=cat;父类的引用指向子类对象
{
animal.speak();
}
void test01()
{
Cat cat;
dospeak(cat);
Dog dog;
dospeak(dog);
}
int main()
{
test01();
cout << "hello" << endl;
system("pause");
return 0;
}
总结
动态多态满足条件:
1、有继承关系
2、子类要重写父类的虚函数,子类中也有父类的成员函数,它与函数重载不一样,重载的参数不一样,但重写的函数返回值类型,函数名,参数列表要完全相同。
3、子类重写时virtual关键字可有可无,但父类必须要有。
动态多态如何使用:
1、父类的指针或引用,指向子类的对象,这样当你运行那个虚函数时,就晚绑定地址。
2.多态原理剖析:内存角度
还是上一个程序,先去掉父类成员函数的virtual,然后新建test02()函数输出sizeof(Animal),此时它是一个空类,大小为1;再把virtual加上,再编译,此时sizeof(Animal)= 4/8看x86或x64
- Animal类内部结构
- 如果没有virtual
Animal类内为空,只有一个字节
- 加上virtual
vfptr :virtual function pointer虚函数指针
指针指向一个虚函数表,这个表是vftable,它是什么结构呢,vftable内 存的就是虚函数表,表的内部会记录虚函数的地址,&Animal::speak
- Cat类内部结构
- 如果没有发生重写的情况,那就是全部继承了
也有一个vfptr指向子类的vftable,这个表的内部记录的继承了父类还是父类虚函数地址&Animal::speak,有4/8个字节
- 如果发生了重写,那么子类中的虚函数表内部会替换成子类的虚函数地址
&Cat::speak,有4/8个字节
当父类的指针或引用指向子类的时候,发生多态
Animal &animal=cat
animal.speak();
3.为什么要用多态,多态有什么优点
- 多态的优点:
- 代码组织结构清晰
- 可读性强
- 利于前期和后期的扩展和维护
分别用普通写法和多态写法实现两个操作数进行运算的计算器类
//普通写法
class Calculator {
public:
int getResult(std::string oper)
{
if (oper == "+")
return m_Num1 + m_Num2;
else if (oper == "-")
return m_Num1 - m_Num2;
else if (oper=="*")
return m_Num1*m_Num2;
//如果想扩展新的功能,需要修改源码
//在真实的开发中,提倡开闭原则
//对扩展进行开放,对修改进行关闭
}
int m_Num1;
int m_Num2;
};
//利用多态实现计算器
//先实现计算器抽象类,方便后续扩展
class AbstractCalsulator {
public:
virtual int getResult();
int m_Num1;
int m_Num2;
};
//加法计算器类
class AddCaculator :public AbstractCalsulator {
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};
//减法计算器类
class SubCaculator :public AbstractCalsulator {
public:
int getResult()
{
return m_Num1 - m_Num2;
}
};
//乘法计算器类
class MulCaculator :public AbstractCalsulator {
public:
int getResult()
{
return m_Num1 * m_Num2;
}
};
//测试案例
void test201()
{
//创建计算器对象
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 20;
cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
}
void test202()
{
//多态使用条件
//父类指针或者引用指向子类对象
AbstractCalsulator*abc = new AddCaculator;
abc->m_Num1 = 100;
abc->m_Num2 = 20;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
//减法运算
abc = new SubCaculator;//只是销毁堆区的数据,但类型没变
abc->m_Num1 = 100;
abc->m_Num2 = 20;
cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
//乘法运算
abc = new MulCaculator;//只是销毁堆区的数据,但类型没变
abc->m_Num1 = 100;
abc->m_Num2 = 20;
cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}
总结
使用多态虽然代码量多了,但是组织结构更清晰,而且下次如果想要添加新的功能,只需要创建新的子类即可,不用在像普通法一样在类中大段代码中找;如果想要修改某个功能,也只需要修改某个子类;对源码避免改动。
组织架构:
4.纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义,我们一般都是调用子类实现,父类的函数即使有功能也用不上,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名(参数列表)=0;
当类中有纯虚函数,也被成为抽象类
- 抽象类的特点:
- 无法实例化对象
- 子类必须要重写抽象类中的纯虚函数,否则子类也属于抽象类
//纯虚函数和抽象类
class Base {
public:
//纯虚函数
//只要有一个纯虚函数,这个类称为抽象类
//抽象类特点
//1、无法实例化对象
//2、抽象类的子类必须要重写父类的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son :public Base {
public:
virtual void func() {
cout << "func函数调用" << endl;
}
};
void test301() {
//Son s;//不重写子类也无法实例化
//子类使用父类的指针调用
Base *base = new Son;
base->func();
}
5.多态案例:制作饮品
制作饮品无论咖啡茶叶对象,大致流程一样:煮水-冲泡-导入杯中-加入辅料
利用多态实现,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
- 创建父类,使用纯虚函数,先把框架搭好,makeDrink()将各种功能整合,但是实现仍然是靠子类实现
class AbstractDrinking {
public:
//煮水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void PutSomething() = 0;
//制作饮品
void makeDrink()
{
Boil();
Brew();
PourInCup();
PutSomething();
}
};
- 在子类中将纯虚函数重写
//咖啡子类
class Coffee :public AbstractDrinking {
public:
//煮水
virtual void Boil() {
cout << "煮水" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡咖啡" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "倒入杯中" << endl;
}
//倒入辅料
virtual void PutSomething() {
cout << "加入牛奶" << endl;
}
};
//茶叶子类
class Tea :public AbstractDrinking {
public:
//煮水
virtual void Boil() {
cout << "煮水" << endl;
}
//冲泡
virtual void Brew() {
cout << "冲泡茶叶" << endl;
}
//倒入杯中
virtual void PourInCup() {
cout << "倒入杯中" << endl;
}
//倒入辅料
virtual void PutSomething() {
cout << "加入枸杞" << endl;
}
};
- 实现函数参数为父类的指针,在函数内部实现,最后释放内存,避免泄露。
void doWork(AbstractDrinking* abs) {
abs->makeDrink();
delete abs;//记得要释放
}
- 测试用例传入子类对象,在主函数调用即可
void test401() {
doWork(new Coffee);
cout << "---------------------" << endl;
doWork(new Tea);
}
6.虚析构和纯虚析构
问题:多态使用时,如果子类有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法
virtual ~类名(){}
纯虚析构语法
virtual ~类名()=0
类名::~类名(){}
- 创建Animal父类
class Animal {
public:
Animal() {
cout << "Animal 构造函数" << endl;
}
~Animal() {
cout << "Animal 虚析构函数" << endl;
}
virtual void speak() = 0;
};
- 创建猫子类,在构造函数时在堆区存属性,析构函数将其析构
class Cat :public Animal {
public:
Cat(string name) {
cout << "Cat构造函数" << endl;
m_Name = new string(name);
cout << *m_Name << endl;
}
virtual void speak() {
cout <<*m_Name<< " 小猫在说话"<<endl;
}
~Cat() {
if (m_Name != NULL) {
cout << "Cat析构函数" << endl;
delete m_Name;
m_Name = NULL;
}
}
public:
string *m_Name;
};
- 测试用例过程:理论上先调用父类构造,再调用子类构造,再调用子类重写虚函数,再析构子类,最后析构父类。但实际上并没有析构子类,是因为析构父类时无法调用子类的析构函数。
void test501() {
Animal * animal=new Cat("Tom");
animal->speak();
//父类的指针在析构的时候不会调用子类的析构函数,导致子类如果有堆区属性,会造成内存泄露
delete animal;
}
- 虚析构和纯虚析构就是为了解决这个问题,修改父类,在析构函数前加virtual即可,如果使用纯虚析构,会报错,是因为纯虚析构也要有实现,在类外实现它。
class Animal {
public:
Animal() {
cout << "Animal 构造函数" << endl;
}
//利用虚析构可以解决父类指针释放子类对象时不干净的问题
//virtual ~Animal() {
//cout << "Animal 虚析构函数" << endl;
//}
//纯虚析构 需要声明也需要有实现的
//有了纯虚析构,这个类也属于抽象类,无法实例化对象
virtual ~Animal() = 0;
//纯虚函数
virtual void speak() = 0;
};
Animal::~Animal() {
cout << "Animal 纯虚析构函数" << endl;
}
总结
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象
- 如果子类没有堆区数据,可以不写虚析构或纯虚析构
- 拥有纯虚析构的类也属于抽象类
7.多态案例:电脑组装
电脑主要组成部件为CPU,显卡,内存条
将每个零件抽象成基类,并且提供不同的厂商的Intel,Lenovo的零件
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作
抽象出每个零件的类
class CPU 抽象类
{
//抽象计算函数
virtual void calculate()=0;
}
class VideoCard 抽象类
{
//抽象显示函数
virtual void display()=0;
}
class Memory 抽象类
{
//抽象存储函数
virtual void storage()=0;
}
电脑类
class Computer
{
构造函数中传入三个零件
提供工作的函数
{
调用每个零件工作的接口
}
}
具体零件厂商
Inter 厂商
class IntelCpu:public CPU
{
void calculate()
{
cout<<"Intel的CPU开始计算了”
}
}
诸如此类
- 创建三种零件基类:三大零件的功能做纯虚函数,让不同厂商的不同零件重写
class CPU
{
public:
virtual void calculate() = 0;
};
class VideoCard
{
public:
virtual void display() = 0;
};
class Memory
{
public:
virtual void storage() = 0;
};
- 创建电脑组装类:组装三台零件作为成员指针;构造函数将传入的零件赋给该电脑对象的成员;电脑工作函数将调用三大零件的功能函数;最后需要析构三种零件。
class Compute
{
public:
Compute(CPU* cpu, VideoCard* videocard, Memory* memory)
{
cout << "构造电脑" << endl;
m_Cpu = cpu;
m_Videocard = videocard;
m_Memory = memory;
}
void dowork()
{
cout << "电脑工作了" << endl;;
m_Cpu->calculate();
m_Videocard->display();
m_Memory->storage();
}
~Compute()
{
if (m_Cpu != NULL || m_Videocard != NULL || m_Memory != NULL)
{
cout << "Compute 析构cpu,videocard,memory" << endl;
delete m_Cpu, m_Videocard, m_Memory;
m_Cpu = NULL;
m_Videocard = NULL;
m_Memory = NULL;
}
}
private:
CPU* m_Cpu;
VideoCard* m_Videocard;
Memory* m_Memory;
};
- 创建零件厂商类:Intel和Levono其实差不多,重写虚函数
//Intel厂商
class IntelCPU :public CPU
{
public:
virtual void calculate()
{
cout << "Intel CPU start calculate" << endl;
}
};
class IntelVideoCard:public VideoCard
{
public:
virtual void display()
{
cout << "Intel VideoCard start display" << endl;
}
};
class IntelMemory :public Memory
{
public:
virtual void storage()
{
cout << "Intel Memory start storage" << endl;
}
};
//Lenovo厂商
class LenovoCPU :public CPU
{
public:
virtual void calculate()
{
cout << "Lenovo CPU start calculate" << endl;
}
};
class LenovoVideoCard :public VideoCard
{
public:
virtual void display()
{
cout << "Lenovo VideoCard start display" << endl;
}
};
class LenovoMemory :public Memory
{
public:
virtual void storage()
{
cout << "Lenovo Memory start storage" << endl;
}
};
- 测试用例:创建电脑对象new一个对象传入三种零件,所以需要new三种厂商零件,然后电脑对象调用工作函数,最后释放内存。
void test601()
{
cout << "test601()" << endl;
//第一台电脑零件,电脑的零件并没有随电脑释放而释放
CPU *intelcpu = new IntelCPU;
VideoCard *intelcard = new IntelVideoCard;
Memory *intelmemory = new IntelMemory;
Compute *compute1=new Compute(intelcpu,intelcard,intelmemory);
compute1->dowork();
delete compute1;
//第二台电脑
Compute *compute2 = new Compute(new IntelCPU, new IntelVideoCard, new IntelMemory);
compute2->dowork();
delete compute2;
}