目录
4.7 多态
4.7.1 多态的基本概念(超级重要)
4.7.2 多态的原理刨析(超级重要)
4.7.2 多态案例一:计算器类
4.7.3 纯虚函数和抽象类
4.7.4 多态案例二 - 制作饮品
4.7.5 虚析构和纯虚析构(重要,很迷)
4.7.6 多态案例三 - 电脑组装
4.7 多态
多态是C++面向对象三大特性之一。
4.7.1 多态的基本概念(超级重要)
多态分为两类:
静态多态和动态多态区别:(超级重要)
下面通过案例进行讲解多态:
P135。
#include
using namespace std;
#include
class Animal
{
public:
// 虚函数
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
class Cat :public Animal
{
public:
// 重写:函数名称、参数列表、函数返回值类型完全相同
// 子类重写的时候,virtual可写可不写,但父类绝对要加
void speak()
{
cout << "小猫在说话" << endl;
}
};
class Dog :public Animal
{
public:
void speak()
{
cout << "小狗在说话" << endl;
}
};
// 执行说话的函数
// 地址早绑定,在编译阶段确定函数地址,所以不管传入什么,都会执行动物类
// 如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,也就是地址晚绑定
void doSpeak(Animal &animal) // Animal &animal = cat
{
animal.speak();
}
// 动态多态满足条件:
// 1.有继承关系
// 2.子类重写父类的虚函数
// 动态多态使用:
// 父类的指针或者引用,指向子类对象
void test01()
{
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}
int main() {
test01();
system("pause");
return 0;
}
------------------------------------------------------------------------------
小猫在说话
小狗在说话
请按任意键继续. . .
现象:
如果 Animal不加 virtual修改,那么输出的是动物在说话。
而Animal加了 virtual修改,那么输出的是小猫小狗在说话。
总结:
父类引用指向子类对象。如果没有发生多态(父类没有加virtual),会造成函数地址早绑定,所以会输出动物,而不是猫。
C++允许父类子类之间的类型转换,不必强制转换。
传入函数体中的参数,类型是动物。而没有发生多态的话,会造成函数地址早绑定,所以会输出动物,而不是猫。
地址早绑定,在编译阶段确定函数地址。所以不管传入什么,都会执行动物类,而不是猫。
把父类写成虚函数后,子类重写虚函数,下方再次调用的时候就会执行晚绑定。
而我们实际传入的是猫,相当于父类的引用指向子类对象。
由于传入对象不同,来执行确定的函数。传入猫就是猫在说话,传入狗就是狗在说话。
函数地址不能提前确定,要看传进来的是什么对象。
如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,也就是地址晚绑定,父类函数加 virtual修改符。
子类重写的时候,virtual可写可不写,但父类绝对要加。
重载:函数名相同,参数列表、函数返回值类型不一样。
重写:函数名相同,参数列表、函数返回值类型相同。
4.7.2 多态的原理刨析(超级重要)
超级重要!!!P136
多态的底层原理:
重写的意义:在于解决继承时,函数名称功能自定义的情形(个人理解)
步骤:
使用上一节例程,将Animal父类的 virtual去掉,输出下 Animal类的占用空间大小,为1个字节。(非静态成员函数不属于类的对象上,分开存储。于是Animal的大小相当于空类,空类为1字节)
virtual还原回来,输出 Animal类的占用空间大小,为4个字节。(虚函数指针,vfptr。virtual function pointer)
vfptr 会指向一个 vftable(虚函数表),表的内部会记录函数地址。
4.7.2 多态案例一:计算器类
案例描述:分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类。
多态的优点:
#include
using namespace std;
#include
// 分别利用普通写法和多态技术实现计算器
// 普通写法
class Calculctor
{
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 test01()
{
Calculctor c1;
c1.m_Num1 = 10;
c1.m_Num2 = 10;
cout << c1.m_Num1 << " + " << c1.m_Num2 << " = " << c1.getResult("+") << endl;
cout << c1.m_Num1 << " - " << c1.m_Num2 << " = " << c1.getResult("-") << endl;
cout << c1.m_Num1 << " * " << c1.m_Num2 << " = " << c1.getResult("*") << endl;
}
// 利用多态实现计算器
// 实现计算器抽象类
class AbstractCalculctor
{
public:
virtual int getResult()
{
return 0;
}
int m_Num1;
int m_Num2;
};
// 加法计算器子类
class Add :public AbstractCalculctor
{
public:
int getResult()
{
return m_Num1 + m_Num2;
}
};
// 减法计算器子类
class Sub :public AbstractCalculctor
{
public:
int getResult()
{
return m_Num1 - m_Num2;
}
};
// 加法计算器子类
class Mul :public AbstractCalculctor
{
public:
int getResult()
{
return m_Num1 * m_Num2;
}
};
void test02()
{
// 加法运算
AbstractCalculctor * c1 = new Add;
c1->m_Num1 = 10;
c1->m_Num2 = 10;
cout << c1->m_Num1 << " + " << c1->m_Num2 << " = " << c1->getResult() << endl;
delete c1;
// 减法
c1 = new Sub;
c1->m_Num1 = 10;
c1->m_Num2 = 10;
cout << c1->m_Num1 << " - " << c1->m_Num2 << " = " << c1->getResult() << endl;
delete c1;
// 乘法
c1 = new Mul;
c1->m_Num1 = 10;
c1->m_Num2 = 10;
cout << c1->m_Num1 << " * " << c1->m_Num2 << " = " << c1->getResult() << endl;
delete c1;
}
int main() {
//test01();
test02();
system("pause");
return 0;
}
------------------------------------------------------------------------
10 + 10 = 20
10 - 10 = 0
10 * 10 = 100
请按任意键继续. . .
普通写法,如果想扩展新的功能,需要修改源码。
在真实开发中,提倡开闭原则:对扩展进行开放,对修改进行关闭
总结:C++开发提倡利用多态设计程序架构,因为多态优点很多。
字节感觉示例不好的地方:
用的堆区,需要释放指针。每次释放都需要重写赋初值,只能实现一种功能,不能通过传参改变功能。
可以在堆区,要先实例化一个子类对象,然后再新建一个父类指针,最后将子类对象的地址赋给父类指针。
4.7.3 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容。
因此可以将虚函数改为纯虚函数。
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了一个纯虚函数,这个类也被称为抽象类。
抽象类特点:
抽象类无法实例化:
抽象类的子类,必须要重写父类的纯虚函数,否则也是抽象类:
4.7.4 多态案例二 - 制作饮品
案例描述:
制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料。
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。
跟 4.7.1 差不多。父类引用指向子类对象,可以有两种写法。
#include
using namespace std;
#include
// 多态案例2 - 制作饮品
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 Tee :public AbstractDrinking
{
public:
// 煮水
virtual void Boil()
{
cout << "煮水" << endl;
}
// 冲泡
virtual void Brew()
{
cout << "冲泡茶叶" << endl;
}
// 倒入杯中
virtual void PourInCup()
{
cout << "倒入杯中" << endl;
}
// 加入辅料
virtual void PutSomething()
{
cout << "加入枸杞" << endl;
}
};
void doMakeDrink1(AbstractDrinking &a)
{
a.makeDrink();
}
void doMakeDrink2(AbstractDrinking *a)
{
a->makeDrink();
delete a;
}
void test01()
{
Tee tee;
doMakeDrink1(tee);
cout << "---------------------------------" << endl;
doMakeDrink2(new Coffee);
}
int main() {
test01();
system("pause");
return 0;
}
----------------------------------------------------------------------------
煮水
冲泡茶叶
倒入杯中
加入枸杞
---------------------------------
煮水
冲泡咖啡
倒入杯中
加入糖和牛奶
请按任意键继续. . .
4.7.5 虚析构和纯虚析构(重要,很迷)
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。堆区数据会造成内存泄漏。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构。
虚析构和纯虚析构共性:
虚析构和纯虚析构区别:
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名 ::~类名(){}
示例:
#include
using namespace std;
#include
// 虚析构和纯虚析构
class Animal
{
public:
// 纯虚函数
virtual void speak() = 0;
Animal()
{
cout << "Animal构造" << endl;
}
~Animal()
{
cout << "Animal析构" << endl;
}
};
class Cat :public Animal
{
public:
Cat(string name)
{
cout << "Cat构造" << endl;
m_Name = new string(name); // 将name放入堆区,并且m_Name指针去维护这个数据
}
virtual void speak()
{
cout << *m_Name << "小猫在说话" << endl;
}
~Cat()
{
if (m_Name != NULL)
{
cout << "Cat析构" << endl;
delete m_Name;
m_Name = NULL;
}
}
string* m_Name;
};
void test01()
{
Animal* animal = new Cat("Tom");
animal->speak();
delete animal;
}
int main() {
test01();
system("pause");
return 0;
}
---------------------------------------------------------------
Animal构造
Cat构造
Tom小猫在说话
Animal析构
请按任意键继续. . .
没有调用Cat析构。
virtual ~Animal()
{
cout << "Animal析构" << endl;
}
将上面 Animal类的析构,前面加 virtual就可以释放的时候执行子类析构了。
利用虚析构可以解决:父类指针释放子类对象的析构
是所有代码都要写虚析构和纯虚析构嘛??不是的,只有子类开辟在堆区才需要,为了解决子类开辟在堆区,析构释放不到的问题。
#include
using namespace std;
#include
// 虚析构和纯虚析构
class Animal
{
public:
// 纯虚函数
virtual void speak() = 0;
Animal()
{
cout << "Animal构造" << endl;
}
利用虚析构可以解决:父类指针释放子类对象的析构
//virtual ~Animal()
//{
// cout << "Animal析构" << endl;
//}
// 纯虚析构 需要声明,也需要实现
// 如果是纯虚析构,该类属于抽象类,无法实例化对象。
virtual ~Animal() = 0;
};
Animal::~Animal()
{
cout << "Animal析构" << endl;
}
class Cat :public Animal
{
public:
Cat(string name)
{
cout << "Cat构造" << endl;
m_Name = new string(name); // 将name放入堆区,并且m_Name指针去维护这个数据
}
virtual void speak()
{
cout << *m_Name << "小猫在说话" << endl;
}
~Cat()
{
if (m_Name != NULL)
{
cout << "Cat析构" << endl;
delete m_Name;
m_Name = NULL;
}
}
string* m_Name;
};
void test01()
{
Animal* animal = new Cat("Tom");
animal->speak();
delete animal;
}
int main() {
test01();
system("pause");
return 0;
}
------------------------------------------------------------------
Animal构造
Cat构造
Tom小猫在说话
Cat析构
Animal析构
请按任意键继续. . .
4.7.6 多态案例三 - 电脑组装
案例描述:
电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)
将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如intel和Lenovo厂商
常见电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
测试时组装三台不同的电脑进行工作。
抽象出每个零件的类。
示例:这小节的例子不大好,P142,看视频吧。