IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C++之类继承和多态(虚函数、构造函数,析构函数) -> 正文阅读

[C++知识库]C++之类继承和多态(虚函数、构造函数,析构函数)

1.虚函数

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

#include<iostream>
#include<string>
using namespace std;
class Flower
{
public:
	virtual void display()//基类的虚函数,形成动态绑定,永远不会运行“动物”,Flower类也从1个字节变成了4个字节(指针)
		//只有在程序运行的时候才知道speak函数具体运行哪个子类的speak函数
	{
		cout << "动物!" << endl;
	}
};

class Rose:public Flower
{
public:	
	void display()//派生类重写了基类的虚函数,覆盖了原来虚函数表中的地址
	{
		cout << "玫瑰" << endl;
	}
};

void fun(Flower &f1)//派生类的对象作为实参,形参可以用基类的引用或者指针来接收,目的在于实现多态性
{//实现了派生类对象向基类类型的转换
	f1.display();//加上virtual之后,这一行就要去看你f1派生对象的类型,去调用相对应的display函数
}

int main() {
	Rose r1;
	fun(r1);
	system("pause");
	return 0;
}

下面通过编译器开发人员命令提示来说明多态的问题,当在基类没有virtual时,这时基类实际上是一个空类,只占1个字节。

?当加上virtual之后,基类变成了4个字节

?多了一个vfptr:virtual?function?pointer虚函数指针(指针4个字节),指向一个虚函数表vftable。

到了派生类,派生类如果没有重写虚函数,此时占4个字节,为从基类继承而来的vfptr,指向了哪个基类的vftable,这个是完完全全从基类继承过来的吧,如下图:

?所以,当你在调用函数调用display的时候,指向的是Flower::display,但本意是要他输出rose的,所以在派生类重写了虚函数之后,再看:

?这样,再调用的时候,就会调用Rose作用域下的display函数了。

由于基类中的虚函数一般不会被执行,那么就直接

virtual void display() = 0;

此时,这个虚函数被称为纯虚函数,这个Flower类就被称为抽象类

当是抽象类的时候,不能实例化对象,包括堆区和栈区,但可以定义一个类指针,比如:

Flower f1;//报错

new Flower;//报错

当一个抽象类作为派生类的父类时,派生类必须要重写这个纯虚函数,否则这个类也将会被定义为抽象类,不能被实例化

这个目的就是要实现多态,重写虚函数

通过一个父类指针,只想多个子类的对象,加强通用性。

?2.虚析构和纯虚析构

?先放上一段代码,不涉及虚析构和纯虚析构,只是我们要再此基础上添内容。

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

class Flower
{
public:
	Flower()
	{
		cout << "Flower的构造函数" << endl;
	}
	virtual void display() = 0;
	~Flower()
	{
		cout << "Flower的析构函数" << endl;
	 }
};

class Rose:public Flower
{
public:	
	Rose()
	{
		cout << "Rose的构造函数" << endl;
	}
	void display()
	{
		cout << "玫瑰" << endl;
	}
	~Rose()
	{
		cout << "Rose的析构函数" << endl;
	}
};
void fun()
{
	Rose r1;
	Flower* f1 = &r1;
	f1->display();
}
int main() {
	fun();
	system("pause");
	return 0;
}

运行结果:

比如说,派生类中有在堆区开辟的一段空间,那么按道理说,应该在派生类的析构函数中去释放,这个需要注意。

但还有一种情况,在调用函数中基类的指针或引用绑定的是堆区的派生类数据。如下所示:

?

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

class Flower
{
public:
	Flower()
	{
		cout << "Flower的构造函数" << endl;
	}
	virtual void display() = 0;
	~Flower()
	{
		cout << "Flower的析构函数" << endl;
	}
};

class Rose :public Flower
{
public:
	Rose(string c)
	{
		color = c;
		cout << "Rose的构造函数" << endl;
	}
	void display()
	{
		cout << "玫瑰" << endl;
	}
	~Rose()
	{
		cout << "Rose的析构函数" << endl;
	}
	string color;
};

void fun()
{
	Flower* f1 = new Rose("blue");//在局部函数定义的f1,利用完就要释放
	f1->display();}

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

?如果你f1不释放,这时候,造成程序内存泄露,两个类始终都不会调用析构函数,直到程序结束。

进一步的,我如果注意到了f1要释放,因此在fun函数中加了一句

delete f1;

这时,我们认为至少是调用函数中的f1内存被释放了,但是,问题在于当调用函数结束后,Rose中的析构函数并没有被执行,原因通过父类指针去释放,会导致子类对象可能清理不干净,即一旦子类对象中有在堆区开辟的数据,在析构中去释放,但这种情况下根本不会去走派生类的析构函数,造成内存泄漏。如下图所示,根本没有Rose的析构:

?

问题总结:当基类指针指向或引用堆区开辟的派生类时,删除父类指针并不会调用子类的析构函数

派生类除了自己的析构函数,还要继承基类的析构函数,用父类的指针默认调用的时父类的析构

要想解决,就要将父类的析构函数加上virtual变成虚析构,这样的话,在析构的时候才会先调用子类的析构,再调用父类的析构,完整程序如下:

#include<iostream>
#include<string>
using namespace std;
class Flower
{
public:
	Flower()
	{
		cout << "Flower的构造函数" << endl;
	}
	virtual void display() = 0;
	virtual ~Flower()
	{
		cout << "Flower的析构函数" << endl;
	}
};
class Rose :public Flower
{
public:
	Rose(string c)
	{
		color = c;
		cout << "Rose的构造函数" << endl;
	}
	void display()
	{
		cout << "玫瑰" << endl;
	}
	~Rose()
	{
		cout << "Rose的析构函数" << endl;
	}
	string color;
};
void fun()
{
	Flower* f1 = new Rose("blue");//在局部函数定义的f1,利用完就要释放
	f1->display();
	delete f1;
}
int main() {
	fun();
	system("pause");
	return 0;
}

?纯虚析构其实比较鸡肋,在析构函数的前面加上virtual,形成纯虚析构,这时候这个类也会被定义为抽象类。但要注意的是,纯虚析构虽然是后边等于0,但是结构上一定要有具体的实现,否则编译就会被报错。

?3.构造函数[下篇会专门推送构造函数]

由上述程序可以看到:
派生类构造时,先构造基类部分,然后再构造派生类部分;派生类析构时,先析构派生类部分,然后再析构基类部分。
因此:在基类构造函数执行的时候,派生类的部分是未定义状态;在基类析构函数执行的时候,派生类的部分已经被释放了。
所以在基类的构造函数或析构函数中调用虚函数是不建议的

?

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-10-08 11:36:46  更:2021-10-08 11:38:16 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/20 3:10:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码