栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

C++核心编程:P10->类和对象----多态

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

C++核心编程:P10->类和对象----多态

本系列文章为黑马程序员C++教程学习笔记,前面的系列文章链接如下
C++核心编程:P1->程序的内存模型
C++核心编程:P2->引用
C++核心编程:P3->函数提高
C++核心编程:P4->类和对象----封装
C++核心编程:P5->类和对象----对象的初始化和清理
C++核心编程:P6->类和对象----C++对象模型和this指针
C++核心编程:P7->类和对象----友元
C++核心编程:P8->类和对象----运算符重载
C++核心编程:P9->类和对象----继承

文章目录
  • 一、多态的基本概念
  • 二、多态的原理剖析
  • 三、案例:计算机类
  • 四、纯虚函数和抽象类
  • 五、案例:制作饮品
  • 六、虚析构和纯虚析构
  • 七、电脑组装


一、多态的基本概念

多态

多态就是指多种状态,它是C++面向对象三大特性之一。
多态分为两类:
①静态多态: 函数重载和运算符重载属于静态多态
②动态多态: 派生类和虚函数实现运行时多态
静态多态和动态多态区别:
①静态多态的函数地址早绑定 - 编译阶段确定函数地址
②动态多态的函数地址晚绑定 - 运行阶段确定函数地址


例: 假设我们有个动物类Animal,还有个继承自动物类的猫类Cat。动物类和猫类都有一个speak成员函数,分别打印动物在说话和猫在说话。现在我们有一个doSpeak函数,函数的参数是动物类的引用Animal& animal。现在我们创建一个猫类对象cat,然后以引用的方式传入doSpeak函数,看看会调用动物类还是猫类的成员函数。

#include 
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(void)
{
	test01();
	return 0;
}

运行,可以看到虽然传入的是猫类的引用,但是speak成员函数是以动物类的引用去接收这个猫类引用,所以现在调用动物的speak成员函数。而我们实现的是:向doSpeak函数传猫类对象就打印猫说话,传狗类对象就打印狗说话。


原因: speak函数的地址是早绑定的,不管传入什么类的对象都会调用动物类的speak成员函数。
解决方案: 在Animal的speak成员函数前面加个virtual,使得Animal的speak成员函数成为一个虚函数,然后在子类中去重写这个speak成员函数。

#include 
using namespace std;

class Animal
{
public:
	//Speak函数就是虚函数
	//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。
	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.speak();
}
//
//多态满足条件: 
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象

void test01()
{
	Cat cat; 
	DoSpeak(cat);
	Dog dog;
	DoSpeak(dog);
}

int main() {
	test01();
	system("pause");
	return 0;
}

运行,可以看到传入小猫对象就打印小猫在说话,传入小狗对象就打印小狗在说话,就实现了运行时多态,即函数地址晚绑定。


总结:

①动态多态满足条件:有继承关系、子类重写父类中的虚函数
②动态多态使用条件:父类指针或引用指向子类对象
③重写:函数返回值类型、函数名、参数列表完全一致称为重写


二、多态的原理剖析

我们分别创建两个类Animal1和Animal2,其中Animal1没有实现动态多态,Animal2实现了动态多态,然后看看它们的大小分别是多少。

#include 
using namespace std;

class Animal1
{
public:
	void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Animal2
{
public:
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

void test02()
{
	cout << "The size of 原始 = " << sizeof(Animal1) << endl;
	cout << "The size of 多态 = " << sizeof(Animal2) << endl;
}

int main() {
	test02();
	system("pause");
	return 0;
}

从前面学习的内容可以知道Animal1中的成员函数和类是分开存储的,所以Animal1是个空类,大小为1个字节。而现在Animal2的大小是4个字节,也就是一个指针的大小。


原理解析:
1、现在我们让父类Animal的成员函数doSpeak是虚函数,然后让子类Cat中什么都没有,只是继承了Animal。

class Animal
{
public:
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat : public Animal
{
};

我们使用VS开发人员工具去看看Animal的结构,可以看到Animal大小确实为4字节,保存了一个虚函数指针,指向了虚函数表。虚函数表中保存的就是Animal类的成员函数speak的地址。

我们使用VS开发人员工具去看看Cat的结构,可以看到Cat大小也为4字节,继承了Animal的虚函数指针,指向了Cat类的虚函数表。而Cat类的虚函数表中保存的是Animal类的成员函数speak的地址。

总结: 子类Cat将父类的虚函数表和虚函数指针都继承下来了。


2、现在我们让子类对父类的虚函数doSpeak进行重写

#include 
using namespace std;

class Animal
{
public:
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat : public Animal
{
public:
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

int main(void)
{
	return 0;
}

我们使用VS开发人员工具去看看Animal的结构,和前面一样没有变化。

我们使用VS开发人员工具去看看Cat的结构,可以看到Cat大小也为4字节,继承了Animal的虚函数指针,指向了Cat类的虚函数表。而Cat类的虚函数表中现在保存的是Cat类的成员函数speak的地址。

总结: 当子类重写父类的虚函数时,子类中的虚函数表中的内容会替换成子类的虚函数地址,而父类中的虚函数表中的内容不变。这时如果父类的指针或引用指向子类对象时,就会发生多态。


三、案例:计算机类

案例描述: 分别利用普通写法和多态技术,设计实现两个操作数进行运算的计算器类


普通写法

#include 
#include 
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;
		}
		//如果要提供新的运算,需要修改源码
		//真实开发中,提倡开闭原则
		//开闭原则:对扩展进行开发,对修改进行关闭
	}
public:
	int m_Num1;
	int m_Num2;
};

void test01()
{
	//普通实现测试
	Calculator c;
	c.m_Num1 = 10;
	c.m_Num2 = 10;
	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;
}

int main() {
	test01();
	system("pause");
	return 0;
}

运行,结果如下。但是当我们需要扩展功能时,如添加除法、积分等操作时,就需要去修改源码。


多态实现

#include 
#include 
using namespace std;

//多态实现
//抽象计算器类
//多态优点:代码组织结构清晰,可读性强,利于前期和后期的扩展以及维护
class AbstractCalculator
{
public:
	virtual int getResult()
	{
		return 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 test02()
{
	//创建加法计算器
	AbstractCalculator* abc = new AddCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << " + " << abc->m_Num2 << " = " << abc->getResult() << endl;
	delete abc;  //用完了记得销毁

	//创建减法计算器
	abc = new SubCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << " - " << abc->m_Num2 << " = " << abc->getResult() << endl;
	delete abc;

	//创建乘法计算器
	abc = new MulCalculator;
	abc->m_Num1 = 10;
	abc->m_Num2 = 10;
	cout << abc->m_Num1 << " * " << abc->m_Num2 << " = " << abc->getResult() << endl;
	delete abc;
}

int main() {
	test02();
	system("pause");
	return 0;
}

运行,结果如下


多态的优点:

①代码组织结构清晰,可读性强
②利于前期和后期的扩展以及维护


四、纯虚函数和抽象类

纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0 ;
当类中有了纯虚函数,这个类也称为抽象类。
抽象类特点:
①无法实例化对象
②子类必须重写抽象类中的纯虚函数,否则也属于抽象类


示例:

class Base
{
public:
	//纯虚函数
	//类中只要有一个纯虚函数就称为抽象类
	//抽象类无法实例化对象
	//子类必须重写父类中的纯虚函数,否则也属于抽象类
	virtual void func() = 0;
};

class Son :public Base
{
public:
	virtual void func() 
	{
		cout << "func调用" << endl;
	};
};

void test01()
{
	Base * base = NULL;
	//base = new Base; // 错误,抽象类无法实例化对象
	base = new Son;
	base->func();
	delete base;//记得销毁
}

int main() {
	test01();
	system("pause");
	return 0;
}

运行,可以看出父类中含有纯虚函数,是个抽象类,子类重写了父类的抽象函数。父类的指针指向子类时,如果调用父类的纯虚函数,这时会调用子类重写后的虚函数。


五、案例:制作饮品

案例描述: 制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料。利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶。

整体代码

//抽象制作饮品
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* drink) {
	drink->MakeDrink();
	delete drink;
}

void test01() {
	DoWork(new Coffee);
	cout << "--------------" << endl;
	DoWork(new Tea);
}

int main() {
	test01();
	system("pause");
	return 0;
}

运行,结果如下


六、虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码。

示例: 创建一个父类Animal,添加纯虚成员函数speak,在构造函数和析构函数中添加打印语句。创建一个子类Cat,重写speak函数,在构造函数和析构函数中添加打印语句。创建一个父类指针指向子类对象,然后父类指针调用父类的speak函数,观察构造和析构函数指向情况。

#include 
#include 
#include 
using namespace std;

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);
	}

	~Cat()
	{
		if (m_Name)
		{
			cout << "Cat析构函数调用" << endl;
			delete(m_Name);
			m_Name = NULL;
		}
	}
	
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}

public:
	string *m_Name;
};

void test01()
{
	Animal *animal = new Cat("Tom");
	animal->speak();
	delete animal;
}

int main()
{
	test01();
	return 0;
}

运行,可以发现没有调用子类的析构函数,导致子类堆区上的数据没被释放,内存泄漏。


原因与解决方案

原因分析:我们是使用父类指针指向子类对象,父类指针在析构时,不会调用子类的析构代码,导致子类如果有堆区的数据会出现内存泄漏情况。
解决方式:将父类中的析构函数改为虚析构或者纯虚析构


虚析构: 直接在父类的析构函数前面加个virtual即可。

纯虚析构: virtual ~Animal()=0。但是直接这样使用会报错,因为Animal类中可能也会存在开辟在堆区上的数据需要释放,这时可以在类外部实现纯虚析构函数。

#include 
#include 
#include 
using namespace std;

class Animal
{
public:
	Animal() 
	{
		cout << "Animal的构造函数" << endl; 
	}

	virtual ~Animal() = 0;
	virtual void speak() = 0;
};

Animal::~Animal()
{
	cout << "Animal的纯虚析构函数" << endl;
}

class Cat : public Animal
{
public:
	Cat(string name)
	{
		cout << "Cat构造函数调用" << endl;
		m_Name = new string(name);
	}

	~Cat()
	{
		if (m_Name)
		{
			cout << "Cat析构函数调用" << endl;
			delete(m_Name);
			m_Name = NULL;
		}
	}
	
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}

public:
	string *m_Name;
};

void test01()
{
	Animal *animal = new Cat("Tom");
	animal->speak();
	delete animal;
}

int main()
{
	test01();
	return 0;
}

运行,可以发现成功调用了Cat的析构函数。


总结

①虚析构和纯虚析构共性:
----可以解决父类指针释放子类对象
----都需要有具体的函数实现
②虚析构和纯虚析构区别:
----如果是纯虚析构,该类属于抽象类,无法实例化对象
③虚析构语法:
virtual ~类名(){}
④纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}


七、电脑组装

案例描述:

  • 电脑主要组成部件为 CPU(用于计算),显卡(用于显示),内存条(用于存储)
  • 将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商
  • 创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口
  • 测试时组装三台不同的电脑进行工作

整体代码如下:

#include 
#include 
#include 
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)
	{
		m_cpu = cpu;
		m_vc = vc;
		m_mem = mem;
	}

	~Computer()
	{
		if (m_cpu != NULL)
		{
			delete m_cpu;
			m_cpu = NULL;
		}
		if (m_vc != NULL)
		{
			delete m_vc;
			m_vc = NULL;
		}
		if (m_mem != NULL)
		{
			delete m_mem;
			m_mem = NULL;
		}
	}

	void work()
	{
		m_cpu->calculate();
		m_vc->display();
		m_mem->storage();
	}
	
private:
	CPU* m_cpu;
	VideoCard* m_vc;
	Memory* m_mem;
};

class IntelCPU : public CPU
{
public:
	virtual void calculate()
	{
		cout << "Intel的CPU开始计算了" << endl;
	}
};

class IntelVideoCard : public VideoCard
{
public:
	virtual void display()
	{
		cout << "Intel的显卡开始显示了" << endl;
	}
};

class IntelMemory : public Memory
{
public:
	virtual void storage()
	{
		cout << "Intel的内存条开始存储了" << endl;
	}
};

class LenovoCPU : public CPU
{
public:
	virtual void calculate()
	{
		cout << "Lenovo的CPU开始计算了" << endl;
	}
};

class LenovoVideoCard : public VideoCard
{
public:
	virtual void display()
	{
		cout << "Lenovo的显卡开始显示了" << endl;
	}
};

class LenovoMemory : public Memory
{
public:
	virtual void storage()
	{
		cout << "Lenovo的内存条开始存储了" << endl;
	}
};

void test01()
{
	CPU* intelcpu = new IntelCPU;
	VideoCard* intelvc = new IntelVideoCard;
	Memory* intelmem = new IntelMemory;
	cout << "第一台电脑开始工作" << endl;
	Computer* computer1 = new Computer(intelcpu, intelvc, intelmem);
	computer1->work();
	delete computer1;

	cout << "-----------------------" << endl;
	cout << "第二台电脑开始工作:" << endl;
	Computer* computer2 = new Computer(new LenovoCPU, new LenovoVideoCard, new LenovoMemory);
	computer2->work();
	delete computer2;

	cout << "-----------------------" << endl;
	cout << "第三台电脑开始工作:" << endl;
	Computer * computer3 = new Computer(new LenovoCPU, new IntelVideoCard, new LenovoMemory);;
	computer3->work();
	delete computer3;
}

int main()
{
	test01();
	return 0;
}

运行,效果如下:

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/947677.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号