C++18 -- 虚析构函数构成多态、纯虚函数、抽象类、虚继承

news2024/9/25 23:21:37

多态的条件:
1)覆盖
2)基类的指针或者引用
虚表的运行原理:

一、多态的特例 – 虚析构函数构成多态

类有指针作为数据成员,必须要写析构函数,如果当前类被继承了,则析构函数写成virtual,
为了实现多态,将子类的空间被合理的释放掉,防止内存泄漏

1、为什么

1)没有加虚的情况示例:

class A
{
public:
	//基类的构造、析构函数析构函数不能继承
	A()
	{
		cout<< "A" <<endl;
		m_i = new int;
	}
	//virtual ~A()//基类的虚析构函数,可以继承
	~A()
	{
		cout << "~A" <<endl;
		delete[] m_i;
	}
private:
	int* m_i;
};
class B:public A
{
public:
	B()
	{
		cout<< "B" <<endl; 
		m_j = new int;
	}
	~B()
	{
		cout << "~B" <<endl;
		delete[] m_j;
	}
private:
	int *m_j;
};
void test(A* pb)
{
	delete pb;
}
//结果为AB~A,没有运行B的析构函数
void main()
{
	A* pb = new B;//定义了一个基类类型的指针pb,让其指向子类对象,pb为A类类型的指针,
	delete pb;
	//test(pb);
}

运行结果:
发现并没有运行基类A的析构函数,造成了内存泄漏。
在这里插入图片描述

2)加上虚:virtual ~A()

而加了虚之后,就正常运行了基类的析构函数,解决了内存泄漏的问题。
在这里插入图片描述

2、虚析构函数:

析构函数是类的一个特殊的成员函数:

1)当一个对象的生命周期结束时,
系统会自动调用析构函数注销该对象并进行善后工作,
对象自身也可以调用析构函数;

2)析构函数的善后工作是:
释放对象在生命期内获得的资源(如动态分配的内存,内核资源);

3)析构函数也用来执行对象即将被撤销之前的任何操作。

根据赋值兼容规则,可以用基类的指针指向派生类对象,如果使用基类型指针指向动态创建的派生类对象,由该基类指针撤销派生类对象,则必须将析构函数定义为虚函数,实现多态性,自动调用派生类析构函数,否则可能存在内存泄漏问题。

总结:在实现运行时的多态,无论其他程序员怎样调用析构函数都必须保证不出错,所以必须把析构函数定义为虚函数。

注意:类中没有虚函数,类中没有指针,就不要把析构函数定义为虚。

在动态分配内存时所有C++的标准库函数都采用这种格式。

3、定义虚函数的规则

类的成员函数定义为虚函数,但必须注意以下几条:

1)派生类中定义虚函数必须与基类中的虚函数同名外,还必须同参数表,同返回类型。否则被认为是同名覆盖,不具有多态性。如基类中返回基类指针,派生类中返回派生类指针是允许的,这是一个例外(协变)。

2)有类的成员函数才能说明为虚函数。这是因为虚函数仅适用于有继承关系的类对象。友元函数和全局函数也不能作为虚函数。

3)静态成员函数,是所有同一类对象共有,不受限于某个对象,不能作为虚函数。

4)内联函数每个对象一个拷贝,无映射关系,不能作为虚函数。

5)构造函数和拷贝构造函数不能作为虚函数。构造函数和拷贝构造函数是设置虚表指针。

6)析构函数可定义为虚函数,构造函数不能定义虚函数,因为在调用构造函数时对象还没有完成实例化(虚表指针没有设置)。在基类中及其派生类中都动态分配的内存空间时,必须把析构函数定义为虚函数,实现撤消对象时的多态性。

7)实现运行时的多态性,必须使用基类类型的指针变量或引用,使该指针指向该基类的不同派生类的对象,并通过该指针指向虚函数,才能实现运行时的多态性。

8)在运行时的多态,函数执行速度要稍慢一些:为了实现多态性,每一个派生类中均要保存相应虚函数的入口地址表,函数的调用机制也是间接实现。所以多态性总是要付出一定代价,但通用性是一个更高的目标。

9)如果定义放在类外,virtual只能加在函数声明前面,不能(再)加在函数定义前面。正确的定义必须不包括virtual。

4、虚函数的默认参数是静态绑定的

虚函数的默认参数是静态绑定的,在子类中重新定义虚函数的时候,并没有重新定义继承而来的参数值。
除非是在实际调用过程中传递了想要的参数。

代码示例:

class Parent
{
public:
	virtual void fn(int a = 10)
	{
		cout << "parent fn a = " << a << endl;
	}
};
class Child:public Parent
{
public:
	virtual void fn(int b = 100)//进行了重写/覆盖
	{
		cout << "child fn b = " << b << endl;
	}
};
void main()
{
	Child cc;  
	Parent *p = &cc;  
	p->fn(200);//child fn b = 200
	p->fn();//产生了动态绑定,变成了10,不是100,
	cc.fn();//为静态绑定,为100
}

运行结果:

在这里插入图片描述

5、虚函数设计

如果使用对象调用,只能调用基类和子类两者共有的函数;
如果没有重写,那么调用也没有意义;

class A
{
public:
	virtual void fn(){ cout << "A::fn" << endl; }
	virtual void ff(){ cout << "A::ff" << endl; }
};
class B:public A
{
public:
	virtual void ff(){ cout << "B::ff" << endl; }
	virtual void fg(){ cout << "B::fg" << endl; }
};
void test(A& a)
{
	//a.fg();//error,不能调用
	//a.fn();//可以调用,但是无意义,没有产生多态
	a.ff();
}
void main()
{
	A a;
	B b;
	test(a);
	test(b);
}

运行结果:

在这里插入图片描述

在这里插入图片描述

6、为什么构造函数不可以是虚函数呢?

1)构造函数的用途:
(1)创建对象,
(2)初始化对象中的属性,
(3)类型转换。

⒉)类中定义了虚函数就会有一个虚函数表(vftable),对象模型中就含有一个指向虚表的指针(__vfptr)。在定义对象时构造函数设置虚表指针指向虚函数表。

3)使用指针和引用调用虚函数,在编译只需要知道函数接口,运行时指向具体对象,才能关联具体对象的虚方法(通过虚函数指针查虚函数表得到具体对象中的虚方法)

4)构造函数是类的一个特殊的成员函数:
(1)定义对象由系统自动调用构造函数,对象自己是不可以调用构造函数;
(2)构造函数的调用属于静态联编,在编译时必须知道具体的类型信息。

5)如果构造函数可以定义为虚构造函数,使用指针调用虚构造函数,如果编译器采用静态联编,构造函数就不能为虚函数。如果采用动态联编,运行时指针指向具体对象,使用指针调用构造函数,相当于已经实例化的对象在调用构造函数,这是不容许的调用,对象的构造函数只执行一次。

6)如果指针可以调用虚构造函数,通过查虚函数表,调动虚构造函数,那么,当指针为nullptr,如何查虚函数表呢?

7)构造函数的调用是在编译时确定,如果是虚构造函数,编译器怎么知道你想构建是继承树上的哪种类型呢?

总结:构造函数不允许是虚函数。

测试代码

class A
{
public:
	A(){ cout << "A" << endl; }
	~A(){ cout << "~A" << endl; }
private:
	int *m_i;
};
class B
{
public:
	B(){m_i = new int; cout << "B" << endl; }
	~B()
	{ 
		cout << "~B" << endl; 
		if( m_i != NULL )//防止显示的调用析构,导致出错
		{
			delete[] m_i;
			m_i = NULL;
	
		}
	}
private:
	int *m_i;
};

A a()//一般不这么写,但语法正确
{
	cout << "aaa" << endl;
	return A();
}

void main()
{
	
	A();//声明了一个局部的无名对象
	A a();//定义了一个函数,返回值为A类类型,函数名为a,函数内无参数
	a();
	
	cout<<endl;

	/*
	A a;
	a.A();//error
	a.~A();//程序员显示的调用析构函数

	B b;
	b.~B();
	*/

}

在这里插入图片描述
在这里插入图片描述

二、纯虚函数

纯虚函数,不需要写实现体。

1、纯虚函数的概念:

纯虚函数(pure virtual function)是指没有具体实现的虚成员函数。它用于这样的情况:设计一个类型时,会遇到无法定义类型中虚函数的具体实现,其实现依赖于不同的派生类。
定义纯虚函数的一般格式为:

virtual 返回类型 函数名(参数表) = 0;

“=O" 表明程序员将不定义该虚函数实现,没有函数体,只有函数的声明;函数的声明是为了在虚函数表中保留一个位置。
“=O"本质上是将指向函数体的指针定义为nullptr。

三、抽象类

包含纯虚函数的类,为抽象类,不能定义对象,但是可以定义抽象类的指针或者引用 来 指向或者引用具体类的对象
抽象类的作用:派生子类,作为类族最上面的基类出现,如果派生出子类,则在子类中必须要全部重写基类中的纯虚函数,其才可称为具体类,如果在子类中,没有实现纯虚函数,则子类也是抽象类

1、抽象类的概念:

含有纯虚函数的类是抽象类。
抽象类是一种特殊的类,它是为抽象的目而建立的,它处于继承层次结构的较上层。
抽象类不能实例化对象,因为纯虚函数没有实现部分,所以含有纯虚函数类型不能实例化对象;

2、抽象类的主要作用:

将相关的类型组织在一个继承层次结构中,抽象类为派生类型提供一个公共的根,相关的派生类型是从这个根派生而来。

3、抽象类的使用规则:

(1) 抽象类只能用作其他类的基类,不能创建抽象类的对象。(2)抽象类不能用作参数类型、函数返回类型或显式类型转换。
(3) 可以定义抽象类的指针和引用,此指针可以指向(引用可以引用)它的派生类的对象,从而实现运行时多态。

4、注意:

抽象类只能作为基类来使用,其纯虚函数的实现由派生类给出。如果派生类没有重新定义纯虚函数,而派生类只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体类型。

四、使用示例:

设计一个:形状类 – 包含矩形、圆形、三角形,分别计算三种图形的周长和面积
或:人类 – 教师类、学生类、工人。。。

1、一种实现示例(没有使用纯虚函数)

class Shape//形状类
{
public:
	//这两个函数不实现又不使用,为什么要写?
	//为了实现后面的覆盖,为了实现多态
	virtual void Area(){}//面积
	virtual void Girth(){}//周长
};
class Circle:public Shape//圆形类
{
public:
	virtual void Area()
	{
		cout << "Circle Area" << endl;
	}
	virtual void Girth()
	{
		cout << "Circle Girth" << endl;
	}
private:
	int m_ra;
};
class Rectangle:public Shape//矩形
{
public:
	virtual void Area()
	{
		cout << "Rectangle Area" << endl;
	}
	virtual void Girth()
	{
		cout << "Rectangle Girth" << endl;
	}
private:
	int m_length;
	int m_width;
};
class Triangle:public Shape//三角形类(这里设计为等边三角形)
{
public:
	virtual void Area()
	{
		cout << "Triangle Area" << endl;
	}	
	virtual void Girth()
	{
		cout << "Triangle Girth" << endl;
	}
private:
	int m_length;
};

//一种可行的调用形式
void test(Shape* p)
{
	p->Area();
	p->Girth();
}
int main()
{
	Shape *pf[3];//指针数组
	pf[0] = new Rectangle;
	pf[1] = new Circle;
	pf[2] = new Triangle;
	for(int i = 0;i < 3 ;i++)
	{
		pf[i]->Area();
		pf[i]->Girth();
		delete pf[i];
		pf[i] = NULL;
	}

	Shape a;
	a.Area();
	a.Girth();

}

在这里插入图片描述

2、使用示例(纯虚函数 – 抽象类)

class Shape//形状类 --为抽象类
{
public:
	//这两个函数不实现又没有办法去写其实现体,为什么要写?
	//为了实现后面的覆盖,为了实现多态
	//纯虚函数,不需要写实现体
	virtual void Area() = 0;//面积
	virtual void Girth() = 0;//周长
};
class Circle:public Shape//圆形类
{
public:
	Circle(int r) :m_ra(r) {}
	virtual void Area()
	{
		cout << "Circle Area = " << 3.14 * m_ra * m_ra << endl;
	}
	virtual void Girth()
	{
		cout << "Girth Girth = " << 2 * 3.14 * m_ra<< endl;
	}
private:
	int m_ra;
};
class Rectangle:public Shape//矩形类
{
public:
	Rectangle(int l,int w):m_length(l),m_width(w){}
	virtual void Area()
	{
		cout << "Rectangle Area = " << m_length * m_width << endl;
	}
	virtual void Girth()
	{
		cout << "Rectangle Girth = " << 2 * (m_length + m_width) << endl;
	}
private:
	int m_length;
	int m_width;
};
class Triangle:public Shape//等边三角形类
{
public:
	Triangle(int l):m_length(l){}
	virtual void Area()
	{
		cout << "Triangle Area = " << endl;
	}	
	virtual void Girth()
	{
		cout << "Triangle Girth = "<< 3 * m_length << endl;
	}
private:
	int m_length;//边长
};

int main()
{
	Shape *pf[3];//指针数组
	pf[0] = new Rectangle(6,6);
	pf[1] = new Circle(6);
	pf[2] = new Triangle(6);
	for(int i = 0;i < 3 ;i++)
	{
		pf[i]->Area();
		pf[i]->Girth();
		delete pf[i];
		pf[i] = NULL;
	}

	//Shape a;//error,抽象类不能定义对象
	//a.Area();
	//a.Girth();

}

运行结果:

在这里插入图片描述

五、虚继承的设计 – 虚基类

虚基类 – 多继承情况下的菱形继承,将相同的属性或操作只保留一份。

示例:

再现实生活中,没有两个没有关系的类,生成第三个类

错误的设计

在这里插入图片描述

class Sofa//沙发类
{
public:
	void sit()
	{
		cout << "sit" << endl;
	}
private:
	int m_size;
};
class Bed//床类
{
public:
	void sit()
	{
		cout << "sit" << endl;
	}
private:
	int m_size;
};
class Sofabed:public Sofa,public Bed
{

};
void main()
{
	Sofabed ss;
	//ss.sit();//error,不明确
	ss.Sofa::sit();//显示调用
	ss.Bed::sit();
}

运行结果:

在这里插入图片描述

菱形设计 – 未添加虚继承 – 不合理设计

在这里插入图片描述

class Furnitrue//家具类
{
public:
	void sit(){ cout << "sit" << endl; }
private:
	int m_size;
};
class Sofa:public Furnitrue//沙发类
{

};
class Bed:public Furnitrue//床类
{

};
class Sofabed:public Sofa,public Bed
{

};
void main()
{
	Sofabed ss;
	Sofa s;
	Bed b;

	cout << sizeof(ss) << endl;
	cout << sizeof(Sofa) << endl;
	cout << sizeof(Bed) << endl;
}

在这里插入图片描述

菱形设计 – 虚继承 – 虚基类 – 合理设计

相同的部分只继承了一份,只在第一次继承的时候完成。

class Furnitrue//家具类
{
public:
	void sit(){ cout << "sit" << endl; }
private:
	int m_size;
};
class Sofa:public virtual Furnitrue//沙发类
{

};
class Bed:public virtual Furnitrue//床类
{

};
class Sofabed:public Sofa,public Bed
{

};
void main()
{
	Sofa s1;
	Bed s2;
	Sofabed ss;

	cout << sizeof(s1) << endl;//为8字节,其中还有一个虚指针,指向当前的虚基类
	cout << sizeof(s2) << endl;//8
	cout << sizeof(ss) << endl;//12,内部还含有两个虚指针,指向虚表,虚表内保存的是偏移量
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/131299.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

四旋翼无人机学习第19节--allgero的板框导入,网表导入

文章目录1 板框导入2 网表导入3 颜色修改4 修改快捷键1 板框导入 1、板框可以在小马哥课程中获取哦。 课程地址:使用Cadence17.2 OrCAD Allegro绘制小马哥DragonFly四轴飞行器 2、下载得到文件&#xff0c;然后用CAD软件查看DXF文件&#xff0c;出现弹框点击是即可(文件只读)。…

科研小白如何做好科研(内附一些科研实用工具)

目录 前言 一、了解自己的研究方向 1、知其然并知其所以然 2、那如何做到呢&#xff1f; 二、拥有良好的科研素养 1、多读文献 2、夯实基础&#xff0c;搞清原理 3、不断学习&#xff0c;擅于总结 4、团队协作&#xff0c;勤沟通&#xff0c;多交流 三、掌握一些…

RHCEansible静态主机清单

首先要做好免密登录 RHCEansible虚拟机初始化配置&#xff0c;ansible配置和安装_无所不知的神奇海螺的博客-CSDN博客 添加主机组 [rootserver ~]# vim /etc/ansible/hosts 或者 测试 [rootserver ~]# ansible node1 -m command -a hostname --- 引号里的是想要受控主机执行的…

Dell inspiron 5488加装硬盘SSD

机械盘真心便宜&#xff0c; 当数据盘很合适。 ———— 我是装双系统&#xff0c; 希望速度快&#xff01; 我就装了一个SSD&#xff0c; STAT接口的&#xff0c; 和机械盘盒一样尺寸&#xff0c; 接口都是SATA&#xff0c; 我买的三星860EVO&#xff0c; 500G&…

I06-python菜鸟教程查漏补缺

学习链接&#xff1a;Python3 教程 | 菜鸟教程 目录 1.基础知识 2.字符串 1.基础知识 多行语句&#xff1a;复数类型&#xff1a; 复数由实数部分和虚数部分构成&#xff0c;可以用 a bj&#xff0c;或者 complex(a,b) 表示&#xff0c; 复数的实部 a 和虚部 b 都是浮点型…

项目的生命周期

0、Preface/Foreword PLM:Product Lifecycle Management System,产品生命周期管理;可以在公司内部或者多个公司之间对于产品研发协同办公,集成与产品相关的人力资源,流程、应用系统和信息,用于支持产品全生命周期的信息创建、管理、分发和应用。 1、硬件产品开发流程 立…

2022年下半年信息系统项目管理师综合知识真题及答案21-40

21、关于项目论证、项目评估目的和作用的描述&#xff0c;不正确的是&#xff1a;&#xff08;&#xff09;。 A&#xff0e;项目论证应围绕市场需求、开发技术、财务经济三个方面开展 B&#xff0e;项目论证的作用是审查可行性研究的可靠性、真实性和客观性 C&#xff0e;项目…

Java8 - Streams map()

文章目录概述Case 1 : A List of Strings to UppercaseCase 2 : List of objects -> List of StringCase 3 : List of objects -> List of other objectsTest概述 Stream.map()是Stream最常用的一个转换方法&#xff0c;它把一个Stream转换为另一个Stream map()方法用于…

网络安全人才市场需求调研

声明 本文是学习2022网络安全人才市场状况研究报告. 下载地址而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 网络安全人才市场需求调研 为了更好地了解网络安全人才市场能力需求情况&#xff0c;我们通过调研问卷对网络安全人才就业市场不同方向的十…

Jetson nano 入手系列之1—如何SSH远程登录

Jetson nano 入手系列之1— 如何SSH远程登录方法1.windows与jetson nano使用同一个网络方法2.windows与jetson nano使用网线连接参考文献本文使用的windows平台来ssh远程登录jetson nano&#xff0c;这里提供两种方法。方法1.windows与jetson nano使用同一个网络 使用同一个网络…

二氧化碳捕获和电化学转化(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

12---实现注册、异常处理,统一封装结果集

1、写统一返回结果包装类 在实际开发中&#xff0c;为了降低开发人员之间的沟通成本&#xff0c;一般返回结果会定义成一个统一格式&#xff0c;具体的格式根据实际开发业务不同有所区别&#xff0c;但至少包括三要素&#xff1a; code状态码&#xff1a;由后端统一定义各种返…

3)Django模板

目录 Django模板 Django 模板标签 变量 ​编辑 列表 字典 过滤器 default length filesizeformat date truncatechars safe if/else 标签 for 标签 {% empty %} ifequal/ifnotequal 标签 注释标签 include 标签 csrf_token 配置静态文件 模板继承 父模板…

Elasticsearch搜索引擎(一)——基础使用

Elasticsearch搜索引擎 关键词是中文的建议使用&#xff0c;英文和数字不要&#xff0c;模糊就行 如果普通数据库查询&#xff0c;无法解决如下问题 如果表记录上千万上亿了这个性能问题&#xff0c;另外一个如果有一个本文字段要在里面模糊配置&#xff0c;这个就会出现严重…

【408篇】C语言笔记-第二十章(数据的机器级表示)

文章目录第一节&#xff1a;补码讲解及内存实战演练1. 补码讲解2. 反码第二节&#xff1a;整型不同类型解析-溢出解析1. 整型不同类型解析2. 溢出解析第三节&#xff1a;IEEE754标准解析第四节&#xff1a;浮点型精度丢失第一节&#xff1a;补码讲解及内存实战演练 1. 补码讲解…

使用华为云服务器跟做尚硅谷电商数仓遇到的问题汇总(持续更新中.......)

文章目录使用xsync时提示xsync:command not found执行lg.sh时显示lg.sh:command not found云服务器网页无法访问hadoop使用xsync时提示xsync:command not found 1.使用xsync时提示xsync:command not found 首先查看是否安装rsync:&#xff08;反正我的里面没有。。。&#xff…

实验十、差分放大电路参数对静态和动态的影响

一、题目 利用Multism研究图1所示差分放大电路在下列情况下对电路静态和动态的影响 &#xff08;1&#xff09;两个 RcR_cRc​ 阻值相差 5%&#xff1b; &#xff08;2&#xff09;RwR_wRw​ 不在中点&#xff1b; &#xff08;3&#xff09;两个差分管的电流放大倍数不相等。…

sql行转列

我们以MySQL数据库为例&#xff0c;来说明行转列的实现方式。 首先&#xff0c;假设我们有一张分数表&#xff08;tb_score&#xff09;&#xff0c;表中的数据如下图&#xff1a; 然后&#xff0c;我们再来看一下转换之后需要得到的结果&#xff0c;如下图&#xff1a; 可以看…

SpringBoot(一)【学习笔记】

1.SpringBoot是什么&#xff1f; Spring Boot是为了简化Spring应用的创建、运行、调试、部署等而出现的&#xff0c;使用它可以做到专注于Spring应用的开发&#xff0c;而无需过多关注XML的配置。 2.SpringBoot的特点 为基于Spring的开发提供更快的入门体验 开箱即用&#xf…

Qt QAbstractItemModel类详解

文章目录一.概述二.QAbstractItemModel类1.类型2.信号3.函数一.概述 QAbstractItemModel 类定义了项目模型必须使用的标准接口&#xff0c;以便能够与模型/视图Model/View框架中的其他组件进行互操作。 正确用法是将其子类化以创建新模型。此类用作 QML 中的项目视图元素或 Qt…