多态实现及原理刨析

系列文章目录

【C++从入门到入土】第一篇:从C到C++.
【C++从入门到入土】第二篇:类和对象基础.
【C++从入门到入土】第三篇:类和对象提高.
【C++从入门到入土】第四篇:运算符重载.
【C++从入门到入土】第五篇:继承(爆肝画图详解).


接下来让我们来一起学习面向对象三大特性的最后一个多态。

前言


一、 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类

  • 静态多态: 函数重载 和 运算符重载属于静态多态,复用函数名
  • 动态多态: 派生类和虚函数实现运行时多态

静态多态和动态多态区别:

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

怎样实现动态多态呢
只需在父类中子类重写的函数名前加上关键字virtual

二、怎么实现动态多态

首先我们来看一下普通继承
父类动物,子类小猫,小狗都具有一个说话的函数speak()
我们知道,父类的指针或引用对象可以指向子类对象,反之则不行。所有我们设立一个全局函数Dospeak(),形参为父类对象,用于调用说话函数
当我们正常运行时
代码如下(示例):

class Animal
{
public:
	//Speak函数就是虚函数
	//函数前面加上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.speak();
}
//
//多态满足条件: 
//1、有继承关系
//2、子类重写父类中的虚函数
//多态使用:
//父类指针或引用指向子类对象

void test01()
{
	//定义两个子类
	Cat cat;
	DoSpeak(cat);

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

在这里插入图片描述
我们可以看到,即使定义了两个不同的子类对象,但它调用的都是父类动物的说话函数,这是因为Dospeak()函数形参是父类对象,发生了静态多态(早绑定)使得不管实参是什么都调用父类的说话函数。
这样的结果与我们的原意不符,我们想要的是传入什么对象就调用对应的函数。
那我们需要怎么做呢?
很简单只需在父类的speak函数前加上关键字即可

virtual void speak()

这样再运行一下
在这里插入图片描述
这样就实现了动态多态(晚绑定)

总结:

多态满足条件

  • 有继承关系
  • 子类重写父类中的虚函数

多态使用条件

  • 父类指针或引用指向子类对象

重写:函数返回值类型 函数名 参数列表 完全一致称为重写

三、动态多态的原理

我们可以先计算一下父类Animal的内存大小
当没有加virtual时类里面只有一个函数,没有成员变量,属于空类,占内存大小为1字节
在这里插入图片描述
当加上virtual关键字时
在这里插入图片描述
变成了4字节,看到4,容易想到int类型和win32下的指针
当用上一张的方法查看类的对象模型时可以发现,类里面多了一个虚函数指针。

在这里插入图片描述

分析完父类来分析子类
不加关键字前
Cat大小
在这里插入图片描述
加上关键字后
在这里插入图片描述
可以看到它继续父类的虚函数指针,变成了4字节
但是我们看它的虚函数表,指针指向发生了改变,指向了它自己的speak函数
这就是多态的原理,它是根据子类里面的虚函数指针来确定它要调用哪个函数,当传入Cat类对象后,speak地址才确定下来。

四、纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法: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;
}

五、虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构

虚析构和纯虚析构共性:

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构区别:

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:

virtual ~类名(){}

纯虚析构语法:

virtual ~类名() = 0;

若该类也有动态开辟的空间,则要到类外实现析构函数
类名::~类名(){}

class Animal {
public:

	Animal()
	{
		cout << "Animal 构造函数调用!" << endl;
	}
	virtual void Speak() = 0;

	//析构函数加上virtual关键字,变成虚析构函数
	//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);
	}
	virtual void Speak()
	{
		cout << *m_Name <<  "小猫在说话!" << endl;
	}
	~Cat()
	{
		cout << "Cat析构函数调用!" << endl;
		if (this->m_Name != NULL) {
			delete m_Name;
			m_Name = NULL;
		}
	}

public:
	string *m_Name;
};

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

	//通过父类指针去释放,会导致子类对象可能清理不干净,造成内存泄漏
	//怎么解决?给基类增加一个虚析构函数
	//虚析构函数就是用来解决通过父类指针释放子类对象
	delete animal;
}
int main() {
	test01();
	system("pause");
	return 0;
}

总结:

​ 1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

​ 2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

​ 3. 拥有纯虚析构函数的类也属于抽象类


总结

以上就是对多态内容的记录,多态在实际中很常用,也是面试,笔试的常考题。感谢阅读,欢迎在评论区对文章的内容指正。
  • 7
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s_persist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值