c/c++入门教程 - 2.4.7 多态、函数地址晚绑定(重写,虚函数,纯虚函数,抽象类,虚析构,纯虚析构)

目录

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 多态的基本概念(超级重要)

多态分为两类:

  • 静态多态函数重载运算符重载属于静态多态,复用函数名。
  • 动态多态派生类虚函数实现运行时多态。(C++说的多态大多是这种)

静态多态和动态多态区别:(超级重要

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址。
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址。

下面通过案例进行讲解多态:

P135。

#include <iostream>
using namespace std;
#include <string>

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修改符。

 

  • 动态多态满足条件:(超级重要
  1. 有继承关系
  2. 子类重写父类的虚函数

 

子类重写的时候,virtual可写可不写,但父类绝对要加。

重载:函数名相同,参数列表、函数返回值类型不一样。

重写:函数名相同,参数列表、函数返回值类型相同。

 

  • 动态多态使用条件:(超级重要
  1. 父类的指针或者引用,指向子类对象。

 

 

4.7.2 多态的原理刨析(超级重要)

超级重要!!!P136

多态的底层原理:

重写的意义:在于解决继承时,函数名称功能自定义的情形(个人理解)

 

步骤

使用上一节例程,将Animal父类的 virtual去掉,输出下 Animal类的占用空间大小,为1个字节。(非静态成员函数不属于类的对象上,分开存储。于是Animal的大小相当于空类,空类为1字节

virtual还原回来,输出 Animal类的占用空间大小,为4个字节。(虚函数指针,vfptr。virtual function pointer)

 

vfptr 会指向一个 vftable(虚函数表),表的内部会记录函数地址。

 

 

4.7.2 多态案例一:计算器类

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

多态的优点:

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

 

#include <iostream>
using namespace std;
#include <string>

// 分别利用普通写法和多态技术实现计算器

// 普通写法
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 <iostream>
using namespace std;
#include <string>

// 多态案例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 <iostream>
using namespace std;
#include <string>

// 虚析构和纯虚析构
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 <iostream>
using namespace std;
#include <string>

// 虚析构和纯虚析构
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,看视频吧。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值