多态
19.1 多态的基本概念
19.1.1 静态多态和动态多态
多态是C++面向对象三大特性之一(封装、继承、多态)
多态分为两类:
- 静态多态:函数重载和运算符重载属于静态多态,复用函数名。
- 动态多态:派生类和虚函数实现运行时多态。
静态多态和动态多态区别:
- 静态多态的函数地址早绑定——编译阶段确定函数地址。
- 动态多态的函数地址晚绑定——运行阶段确定函数地址。
下面通过案例进行讲解多态:
#include<iostream>
using namespace std;
//多态
//动物类
//地址早绑定,在编译阶段就确定了函数地址
class Animal
{
public:
void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
m_A = 100;
}
int m_A;
};
//执行说话的函数
void doSpeak(Animal& animal) //Animal& animal = cat; 传入的是子类,用父类接收,此时此对象转换为了父类
{
cout << (typeid(animal).name()) << endl;
animal.speak();
}
//如果想要执行让猫说话,那么这个函数地址就不能早绑定,需要在运行阶段进行绑定,也就是晚绑定
//动态多态
//1.有继承关系
//2.子类要重写父类的虚函数
//动态多态的使用
//使用动态的指针或者引用来执行子类对象
class Animal_02
{
public:
virtual void speak() //虚函数
{
cout << "动物在说话" << endl;
}
};
class Cat_02 :public Animal_02
{
public:
//函数重写 函数返回值类型 函数名 参数列表 完全不同 子类中的virtual可写可不写
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog_02 :public Animal_02
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
void doSpeak_02(Animal_02& animal) //这里仍旧是子类,因为父类中有虚函数,所以在这里
{
cout << (typeid(animal).name()) << endl;
animal.speak();
animal.Animal_02::speak();
}
void test1_01()
{
Cat cat1;
doSpeak(cat1);
Cat_02 cat2;
doSpeak_02(cat2);
Dog_02 dog;
doSpeak_02(dog);
//通过晚绑定,使得函数地址不是一开始就绑定完毕, 而是通过不同的对象指针来绑定不同的函数地址。
}
int main()
{
test1_01();
system("pause");
return 0;
}
总结:
动态多态的满足条件:
- 有继承关系。
- 子类要重写父类的虚函数。
动态多态的使用:
使用父类指针或者引用来执行子类对象。
19.1.2 多态的原理刨析
以上面的代码为例,当写了虚函数后,在类中会发生一些变化。
在Animal_02类中,会储存一个虚函数指针——vfptr(virtual function pointer),此指针指向一个虚函数表——vftable(virtual function table),在虚函数表的内部记录虚函数的地址。
在虚函数表中储存函数的方式就是将此函数的作用域一并储存起来,储存的是它的地址,即:&Animal_02::speak
。
当子类(cat_02)继承父类(Animal_02)时,子类继承了父类的虚函数指针和虚函数表。
此时:
-
若子类没有重写父类的虚函数,那么子类的虚函数指针和虚函数表完全与父类相同;
-
若子类重写了父类的虚函数,那么在子类的虚函数表中子类重写的函数会将父类的虚函数覆盖。
注:以上是多态的原理刨析,在实际应用中,当父类出现虚函数,无论子类是否重写此虚函数,通过 typeid(对象).name() 可以发现即使使用父类指针/引用指向子类对象,该对象的类型也是子类的类型,而不像往常一样被更改为父类类型。
也就是说当父类出现虚函数时,即使出现父类指针/引用指向子类对象的代码,这时此对象仍旧是子类类型,可以像正常被继承下来的子类一样使用访问子类和父类的语法。
多态有但是继承所没有的功能:
如果在父类定义了一个成员函数(假设名字叫做A)用来调用虚函数(假设名字叫做B),此时根据我们的原理, 我们用子类继承父类并重写虚函数B, 此后子类对象调用父类对象的成员函数A时,这个A所调用的虚函数B已经被子类重写过的函数覆盖,所以会直接调用被覆盖之后的函数B。也就是说我们不需要再在子类中写一个新的A来调用新的B,直接通过此子类对象调用父类中的函数A即可。
这样一来,很多重复调用的函数我们就不需要在子类中一遍一遍重写了。
19.2 多态案例1-计算器
案例描述:
分别利用普通写法和多态技术,设计实现两介操作数进行运算的计算器类。
多态的优点:
- 代码组织结构清晰(抽象类和子类很清晰可读)。
- 可读性强 (每个功能都在一个类里,方便读)。
- 利于前期和后期的扩展以及维护(直接创建或者修改一个新的子类即可)。
#include<iostream>
using namespace std;
//普通写法
class Calculator
{
public:
int getResult(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;
};
void test2_01()
{
cout << "普通实现" << endl;
Calculator c;
c.m_Num1 = 10;
c.m_Num2 = 20;
cout << c.m_Num1 << " + " << c.m_Num2 << " = " << c.getResult("+") << endl;
cout << c.m_Num1 << " - " << c.m_Num2 << " = " << c.getResult("-") << endl;
cout << c.m_Num1 << " * " << c.m_Num2 << " = " << c.getResult("*") << endl;
}
//利用多态实现计算器
//实现计算器抽象类
class AbstractCalculator
{
public:
virtual int getResult() = 0;
int m_Num1;
int 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;
}
};
class MulCalculator :public AbstractCalculator
{
public:
int getResult()
{
return m_Num1 * m_Num2;
}
};
void test2_02()
{
cout << "多态实现" << endl;
//多态使用条件
//父类指针或者引用指向子类对象
//加法
AbstractCalculator* abc = new AddCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 20;
cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
abc = new SubCalculator; //重新规定指针指向即可
abc->m_Num1 = 10;
abc->m_Num2 = 20;
cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
abc = new MulCalculator;
abc->m_Num1 = 10;
abc->m_Num2 = 20;
cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
delete abc;
}
int main()
{
test2_01();
test2_02();
system("pause");
return 0;
}
19.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。
因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名(参数列表) = 0;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
-
无法实例化对象。
-
子类必须重写抽象类中的纯虚函数,否则也属于抽象类。
示例:
#include<iostream>
using namespace std;
class Base
{
public:
//只要有一个纯虚函数,这个类就称为抽象类
virtual void func() = 0;
};
class Son :public Base
{
public:
int m_A;
};
class Son_02 :public Base
{
public:
void func()
{
cout << "func的调用" << endl;
}
int m_A;
};
int main()
{
//Base b; //无法实例化对象
//Base* b = new Base; //堆区也是不可以的
//Son s; //子类不重写父类纯虚函数时,也会被认为是抽象类
Son_02 s; //重写虚函数后,可以实例化
Base* base = new Son_02;
base->func();
system("pause");
}
19.4 多态案例2-制作饮品
案例描述:
制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料。
利用多态技术实现本案例:提供抽象制作饮品基类,提供制作咖啡和茶叶的子类。
示例:
#include<iostream>
using namespace std;
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:
//煮水
void Boil()
{
cout << "煮农夫山泉" << endl;
}
//冲泡
void Brew()
{
cout << "冲泡咖啡" << endl;
}
//倒入杯中
void PourInCup()
{
cout << "倒入杯中" << endl;
}
//加入辅料
void PutSomething()
{
cout << "加入糖和牛奶" << endl;
}
};
//茶叶
class Tea :public AbstractDrinking
{
public:
//煮水
void Boil()
{
cout << "煮开水" << endl;
}
//冲泡
void Brew()
{
cout << "冲泡茶叶" << endl;
}
//倒入杯中
void PourInCup()
{
cout << "倒入杯中" << endl;
}
//加入辅料
void PutSomething()
{
cout << "加入枸杞和柠檬" << endl;
}
};
void dowork(AbstractDrinking* abs)
{
abs->makeDrink();
delete abs;
}
void test4_01()
{
cout << "制作咖啡:" << endl;
dowork(new Coffee);
cout << "制作茶叶:" << endl;
dowork(new Tea);
}
int main()
{
test4_01();
system("pause");
return 0;
}
19.5 虚析构和纯虚析构
问题:多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用子类的析构代码。因为当析构函数不是虚函数时,这个析构函数没有虚函数指针,而多态的情况下都是通过虚函数指针调用函数的,所以将不会调用整个析构链。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
虚函数和纯虚函数析构共性:
- 可以解决父类指针释放子类对象,即可以调用子类的析构函数。
- 都需要有具体的函数实现,纯虚析构也要有函数实现,使用内外定义即可。
虚析构和纯虚析构区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象。
虚析构语法:
virtual ~类名(){
}
纯虚析构语法:
//类内声明
virtual ~类名() = 0;
//类外定义
类名::~类名(){
}
示例:
#include<iostream>
using namespace std;
class Animal5
{
public:
Animal5()
{
cout << "Animal5构造函数调用" << endl;
}
//虚析构
//virtual ~Animal5()
//{
// cout << "Animal5析构函数调用" << endl;
//}
//纯虚析构
virtual ~Animal5() = 0;
//纯虚函数
virtual void speak() = 0;
};
Animal5::~Animal5()
{
cout << "Animal5析构函数调用" << endl;
}
class Cat5 :public Animal5
{
public:
Cat5(string name)
{
cout << "Cat5构造函数" << endl;
m_Name = new string(name);
}
virtual void speak()
{
cout << *m_Name << "小猫在说话" << endl;
}
~Cat5()
{
cout << "Cat5析构函数" << endl;
if (m_Name != NULL)
{
delete m_Name;
m_Name = NULL;
}
}
string* m_Name; //创建在堆区
};
void test5_01()
{
Animal5* animal = new Cat5("汤姆");
animal->speak();
delete animal;
}
int main()
{
test5_01();
system("pause");
return 0;
}
总结:
- 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象。
- 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构。
- 拥有纯虚析构函数的类也属于抽象类,不能实例化。
19.6 多态案例3-电脑组装
电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)。
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂 商和Lenovo厂商。
创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口。
测试时组装三台不同的电脑进行工作。
示例:
#include<iostream>
using namespace std;
class CPU
{
public:
//抽象计算函数
virtual void calculate() = 0;
};
class VideoCard
{
public:
//抽象显示函数
virtual void display() = 0;
};
class Memory
{
public:
//抽象存储函数
virtual void storage() = 0;
};
//传入零件的型号,使得电脑工作。
class Computer
{
public:
Computer(CPU* cpu, VideoCard* vc, Memory* mem)
{
this->cpu = cpu;
this->vc = vc;
this->mem = mem;
}
void work()
{
cpu->calculate();
vc->display();
mem->storage();
}
~Computer()
{
if (cpu != NULL)
{
delete cpu;
cpu = 0;
}
if (vc != NULL)
{
delete vc;
vc = 0;
}
if (mem != NULL)
{
delete mem;
mem = 0;
}
}
private:
CPU* cpu;
VideoCard* vc;
Memory* mem;
};
//Inter
class InterCPU :public CPU
{
public:
void calculate()
{
cout << "Intel的CPU开始计算了" << endl;
}
};
class InterVideoCrad :public VideoCard
{
public:
void display()
{
cout << "Inter的显卡开始显示了" << endl;
}
};
class InterMemory :public Memory
{
public:
void storage()
{
cout << "Inter的内存条开始存储了" << endl;
}
};
//Lenovo
class LenovoCPU :public CPU
{
public:
void calculate()
{
cout << "Lenovo的CPU开始计算了" << endl;
}
};
class LenovoVideoCrad :public VideoCard
{
public:
void display()
{
cout << "Lenovo的显卡开始显示了" << endl;
}
};
class LenovoMemory :public Memory
{
public:
void storage()
{
cout << "Lenovo的内存条开始存储了" << endl;
}
};
void test6_01()
{
//创建第一台电脑
cout << "第一台电脑开始工作" << endl;
CPU* intercpu = new InterCPU;
VideoCard* intercard = new InterVideoCrad;
Memory* intermemory = new InterMemory;
Computer* computer1 = new Computer(intercpu, intercard, intermemory);
computer1->work();
delete computer1;
//创建第二台电脑
cout <<"-----------------\n" << "第二台电脑开始工作" << endl;
CPU* lenovocpu = new LenovoCPU;
VideoCard* lenovocard = new LenovoVideoCrad;
Memory* lenovomemory = new LenovoMemory;
Computer* computer2 = new Computer(lenovocpu, lenovocard, lenovomemory);
computer2->work();
delete computer2;
//创建第三台电脑
cout << "-----------------\n" << "第三台电脑开始工作" << endl;
Computer* computer3 = new Computer(new LenovoCPU, new InterVideoCrad, new LenovoMemory);
computer3->work();
delete computer3;
}
int main()
{
test6_01();
system("pause");
return 0;
}