(十)C++基础之类的多态

1.1、多态的含义

如果有几个上似而不完全相同的对象,有时人们要求在向它们发出同一个消息时, 它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。

C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。

多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统 升级,维护,调试的工作量和复杂度。

这才是面向对象真正实用的东西,其实前面类的封装、和继承在C语言中都可以实现,但是多态是属于面向对象的一个特点。

1.2、多态发生的条件

我们先来看一段代码:

#include <iostream>

using namespace std;

class Student
{
public:
	void doing(void)
	{
		cout << "学习数学中" << endl;
	}
private:
};

class Sporter :public Student
{
public:
	void doing(void)
	{
		cout << "打球" << endl;
	}
private:
};

void doing(Student &s)
{
	s.doing();
}

int main(void)
{
	Student s1;

	Sporter t1;

	doing(s1);
	cout << "-----------------------" << endl;
	doing(t1);

	return 0;
}

这段代码中,我们定义一个学生类和一个体育生类,体育生继承学生,然后打印他们此刻正在做的事情。

前面我们学习继承的时候可以知道,父类的引用可以传递子类的对象,我们来看一下运行结果。

在这里插入图片描述
我们想要输出的是这两个学生都在干嘛,但是我们发现最终都是输出学生正在学习,也就是父类的doing函数,那么这时候我们因该如何改变来达到我们的目的呢?

在父类中想要执行的函数加上关键字virtual,例如:

class Student
{
public:
	virtual void doing(void)
	{
		cout << "学习数学中" << endl;
	}
private:
};

但是为了方便阅读,建议在子类中的需要执行的函数中也添加这个关键字,这个关键字在子类中是否添加都不会影响到,重点是要在父类中关键函数添加virtual。

class Sporter :public Student
{
public:
	virtual void doing(void)
	{
		cout << "打球" << endl;
	}
private:
};

之后我们在运行这个代码。
在这里插入图片描述

发现得到我们想要的结果了,这就是多态,我可以传递不同的对象去执行不同的函数,来达到我想要的目的。

通过这个,总结一下发生多态的三个条件:

1、需要有继承。
2、需要有虚函数(添加virtual)。
3、父类指针或者引用指向子类对象。

使用多态的意义:

可以一劳永逸,无线拓展。

1.3、虚析构函数

构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。
析构函数可以是虚的。虚析构函数用于指引delete 运算符正确析构动态对象 。

例如:

#include <iostream>

using namespace std;

class Student
{
public:
	virtual void doing(void)
	{
		cout << "学习数学中" << endl;
	}

	virtual ~Student()
	{
		cout << "~Student()" << endl;
	}
private:
};

class Sporter :public Student
{
public:
	virtual void doing(void)
	{
		cout << "打球" << endl;
	}
	~Sporter()
	{
		cout << "~Sporter()" << endl;
	}
private:
};



void doing(Student *s)
{
	delete s;
}

int main(void)
{
	Sporter *s1 = new Sporter;


	doing(s1);

	return 0;
}

这个是在析构函数前面加上virtual关键字的运行结果:
在这里插入图片描述
没有添加virtual关键字:

class Student
{
public:
	virtual void doing(void)
	{
		cout << "学习数学中" << endl;
	}

	 ~Student()
	{
		cout << "~Student()" << endl;
	}
private:
};

这个是没有在父类的析构函数加上virtual的运行结果:
在这里插入图片描述

从这里我们就能对比出来,如果在父类中的析构函数中没有添加上虚函数virtual这个关键字,在调用父类指针指向的子类对象时候,他并没有调用子类对象的析构函数,那么这就有可能导致内存泄漏,就是没法调用子类析构函数将子类申请的内存释放,所以一般都建议父类的析构函数都添加上virtual成为虚函数。

1.4、重载、重写、重定义

1、重载(添加):
a 相同的范围(在同一个类中)
b 函数名字相同
c 参数不同
d virtual关键字可有可无

2、重写(覆盖) 是指派生类函数覆盖基类函数,特征是:
a 不同的范围,分别位于基类和派生类中
b 函数的名字相同
c 参数相同
d 基类函数必须有virtual关键字

3、重定义(隐藏) 是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
a 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
b 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。

1.5、多态实现的原理

1、虚函数表和vptr指针

a 当类中声明虚函数时,编译器会在类中生成一个虚函数表;
b 虚函数表是一个存储类成员函数指针的数据结构;
c 虚函数表是由编译器自动生成与维护的;
d virtual成员函数会被编译器放入虚函数表中;
e 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。

其实说白一点,就是如果类中定义有虚函数,那么编译器会开辟一个虚函数表,然后将vptr指针指向这个表,父类有自己的虚函数表,子类也有自己的虚函数表,当我们调用最终的时候是调用vptr这个表里面的函数,实际上十指向虚函数表里面的函数,这样就可以达到不同对象调用不同的函数了。

2、如何证明vptr的存在
我们看一下这段代码:

#include <iostream>

using namespace std;

class Student
{
public:
	void doing(void)
	{
		cout << "学习数学中" << endl;
	}

private:
	int a;
};

class Student2
{
public:
	virtual void doing(void)
	{
		cout << "学习数学中" << endl;
	}

private:
	int a;
};




int main(void)
{
	cout << "student  大小:" << sizeof(Student) << endl;
	cout << "Student2 大小:" << sizeof(Student2) << endl;

	return 0;
}

这里定义了两个类,其中一个类是存在虚函数的,另外一个类是不存在虚函数的,前面我们证明过函数是不在类空间内的,如果类中没有vptr指针,那么理论上应该只有一个整型变量a,也就是大小为4.

我们看一下运行结果:
在这里插入图片描述

但是我们发现存在虚函数的类大小是8多了4个字节,而这个4个字节就是vptr的大小。

3、vptr指针的分布初始化

讲到这里我们就会有一个疑惑,能否在父类的构造函数里面调用虚函数,我们写个代码测试一下:

#include <iostream>

using namespace std;

class Student
{
public:
	Student()
	{
		doing();
	}
	virtual void doing(void)
	{
		cout << "学习数学中" << endl;
	}

private:
};

class spoerter :public Student
{
public:
	spoerter()
	{
		doing();
	}
	virtual void doing(void)
	{
		cout << "运动中" << endl;
	}

private:
};



int main(void)
{
	spoerter s1;

	return 0;
}

运行结果:
在这里插入图片描述
我们只创建spoerter这个类的对象,那么为什么会输出父类的构造函数呢?
因为在我们创建子类的时候,也会调用父类的构造函数,但是这里父类的构造函数输出的却是自己的doing这个函数,后面那个才是在子类构造函数中的doing,他不是虚函数么?为什么没法实现呢?

我们来看这一张图:
在这里插入图片描述

首先子类创建vptr指针的时候,在调用父类构造函数,他是指向父类的虚函数表,只有到了子类的构造函数执行的时候,才会把vptr指针指向子类的虚函数表,所以里有个vptr分布初始化的过程。

所以我们一般不建议在构造函数里面进行一些任务的执行,我们只需要在里面初始化成员变量即可。

1.6、纯虚函数和抽象类

在java(我没学过java,听老师讲的)中,存在接口这么一说,但是C++中并没有接口,它存在的是一个叫做 纯虚函数 的概念。

1、基本概念
 纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本,纯虚函数为个派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)。

2、纯虚函数的格式
在这里插入图片描述

我们举个例子:

#include <iostream>

using namespace std;

class Graphics
{
public:

	virtual void get_area(void) = 0;

private:
};



int main(void)
{
	Graphics g1;



	return 0;
}

我们并没实现纯虚函数的定义,所以实际上编译器给我们提示出错了。
在这里插入图片描述
他在这个过程中只是占用一个位置,但是因为没有实现,当你通过类创建对象时,就可以调用这个函数,可是实际上这个函数还没有意义,所以编译器报错了。

我们来看下面这个例子。

#include <iostream>

using namespace std;

class Graphics
{
public:

	virtual void get_area(void) = 0;

private:
};

//三角形
class Triangle :public Graphics
{
public:
	Triangle(int w,int h)
	{
		this->w = w;
		this->h = h;
	}
	virtual void get_area(void)
	{
		cout << "三角形面积:" << w*h / 2 << endl;
	}
private:
	int w;
	int h;
};

//长方体
class Cuboid :public Graphics
{
public:
	Cuboid(int w, int h)
	{
		this->w = w;
		this->h = h;
	}
	virtual void get_area(void)
	{
		cout << "长方体面积:" << w*h << endl;
	}
private:
	int w;
	int h;
};



int main(void)
{
	Graphics *pg = NULL;

	Triangle t1(10,20);
	Cuboid c1(10, 20);



	pg = &t1;

	pg->get_area();


	pg = &c1;

	pg->get_area();


	return 0;
}

我们定义一个图形总类,定义一个获取面积的纯虚函数。
运行结果:
在这里插入图片描述
通过虚函数,只要我父类的纯虚函数未改变,那么我可以创建无数个子类,这样我就可以达到同一个功能,不同子类的实现,这就相当于抽象出来一个接口,我不管你如何实现,我最终调用那个函数,就能执行到我想要的结果。

这样的好处就是我做开发的时候,我可以不用动我之前写过的代码,只需要重新写一个类,按照父类的定义来写,这样就可以扩充我代码框架的内容,维护起来也很容易。

1.7、自己的一些总结

写到这一篇,C++的基础就已经告一段落了,这是篇文章我是参考这个文件来写的《轻松搞定C++语言.pdf》这个文档适合有C语言基础的同学,然后扩充来写,学习C++会很快入手。

有兴趣的同学可以去下载来看看,这篇文章总结的很到位,学习起来相对容易,下载地址:
链接:https://pan.baidu.com/s/17Ael6VXhLxfxfLm8lv_Fcw
提取码:scd3

发布了29 篇原创文章 · 获赞 0 · 访问量 413

猜你喜欢

转载自blog.csdn.net/weixin_42547950/article/details/104433245