【C++】多态

news2024/11/17 22:33:36

1.多态  

1.1多态的概念:

多态:就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。李明要吃饭,那就要吃早饭,午饭,晚饭,而不是一天只吃午饭这种单一情况。

多态分为:静态多态动态多态

我们原来写的重载的交换函数,参数类型有int,double fioat等等。这就构成了静态多态。这里的静态是指编译时

1.2多态构成条件 (动态多态)

  • 1. 必须通过基类的指针或者引用调用虚函数(被virtual修饰的函数)
  • 2. 被调用的函数必须是虚函数(virtual),且派生类必须对基类的虚函数进行重写(函数名、返回值、参数均相同 + 虚函数)这个virtual和继承里的那个用法不同,简称一词多义。

 接下来我们把图中的代码写一下

class Food  //基类
{
public:
	virtual void eatfood()   // 虚函数
	{
		cout << "eat food--基类" << endl;
	}
};

class Breakfast :public Food
{
public:
	virtual void eatfood()
	//void eatfood()  子类不需要加virtual也可以和父类的构成重写,但是不规范且有许多漏洞。
	{
		cout << "eat breakfast---派生" << endl;
	}
};

class Lunch :public Food
{
public:
	virtual void eatfood()
	{
		cout << "eat Lunch----派生" << endl;
	}
};
void func1(Food e)   //吃饭
{
	e.eatfood();
}
void func2(Food* e)   //吃饭
{
	e->eatfood();
}
void func3(Food& e)   //吃饭
{
	e.eatfood();
}
int main()
{
	Food f;
    Breakfast bf;
	Lunch l;
	func1(f);
	func1(bf);
	func1(l);
	//父类指针
	Food *pf=&f;
	Breakfast *pbf=&bf;
	Lunch *pl=&l;
	func2(pf);
	func2(pbf);
	func2(pl);
	//父类引用
	func3(f);
	func3(bf);
	func3(l);
}
  • 1,func函数中用父类对象。会破坏多态条件,不构成多态。

  •  2.派生了的虚函数的virtual去掉也可以构成多态。但是这个写法极其不规范,也有很大漏洞。
  • 3.基类的虚函数去掉virtual就不构成多态了,切记!

1.3虚函数重写的两大例外

  • 1.协变:

生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。通俗的说是虚函数返回值类型为父子关系指针和引用

class Food  //基类
{
public:
	virtual Food* eatfood()   // 虚函数
	{
		cout << "eat food--基类" << endl;
		return new Food;
	}
};
class Breakfast :public Food
{
public:
	virtual Breakfast* eatfood()
		
	{
		cout << "eat breakfast---派生" << endl;
		return new Breakfast;
	}
};

int main()
{
	Food f;
	Food* pf = &f;
	f.eatfood();
	Breakfast bf;
	Breakfast* pbf=&bf;
	pbf->eatfood();
}

 因为有协变的存在,所以虚函数重写后的返回值不一定相同 。

  • 2.析构函数的重写(子类与父类析构函数的名字不同)

如果父类的析构函数为虚函数,此时子类析构函数只要定义,无论是否加virtual关键字,都与父类的析构函数构成重写,虽然父类与子类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

class Food  //基类
{
public:
	virtual ~Food()   // 虚函数
	{
		cout << "~Food()" << endl;
	}
};
class Breakfast :public Food
{
public:
	 ~Breakfast()
	{
		cout << "~Breakfast()---派生" << endl;
	}
};

int main()
{
	Food f;
	Breakfast bf;
}


 

  • 如果我们把父类的virtual去掉

  • 结果没发生啥变化,但是我创建一个基类指针,指向派生类我们再看一下。 

  •  为啥全部都是指向父类了?如果我们在恢复virtual看一下

 把virtual在加上,就又恢复了。

为了方便解释,我们在改一下

  •  delete需要做两步:1.调用析构

                                     2.函数释放空间

  • pf和pbf的空间都会被释放,pf指向父类对象,期望调用父类的析构函数,pbf指向子类对象,期望调用子类的析构函数,指向父类调父类,指向子类调子类,期望这里达到多态行为,虽然没有明显的函数调用,但是delete操作调了析构函数。
  • pbf指向子类对象,但是发现没有调用子类析构函数,可能存在内存泄漏

补充:继承中说的隐藏:

  1. 子类和父类的析构函数构成隐藏。因就是表面上子类的析构函数个父类的析构函数名不同,但是为了构成重写,编译器会对析构函数名调用时,统一将父类和子类的析构函数名改成destructor( )。统一改成destructor( )构成隐藏的目的就是在这里能够调用同一个函数,达到多态指向父类对象就调父类对象,指向子类对象就调子类对象的的目的。
  2. 因此,父类函数中的virtual不能省,否则子类继承不了父类的virtual属性,无法重写父类的虚函数,如果这个函数是析构函数,那么还会造成内存泄漏。为了保持统一,父类和子类虚函数前面的virtual都不要省。

2.C++11 override 和 final

  • final:如果修饰虚函数表示不想被重写;如果修饰类,表示不想被继承;都是在这个类,虚函数后面加final。

  • override:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

3.重写,重载,隐藏

  •  虚函数重写也叫做覆盖,在子类中重写了一个和父类中的虚函数完全相同的虚函数:包括函数名、返回值、参数列表都相同,这时候子类就重写了父类的虚函数。
  • 子类重写的虚函数的函数名、返回值、参数列表和父类一定要完全相同,否则就变成了函数重载

重载和重写的区别:

  1. 定义不同---重载是定义相同的方法名,参数不同;重写是子类重写父类的方法。
  2. 范围不同---重载是在一个类中,重写是子类与父类之间的。
  3. 多态不同---重载是编译时的多态性,重写是运行时的多态性。
  4. 返回不同---重载对返回类型没有要求,而重写要求返回类型必须相同。
  5. 参数不同---重载的参数个数、参数类型、参数顺序可以不同,而重写父子方法参数必须相同。
  6. 修饰不同---重载对访问修饰没有特殊要求,重写访问修饰符的限制一定要大于被重写方法的访问修饰符。

4.抽象类

4.1.纯虚函数

格式:在虚函数后面写=0。

抽象类:

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。 

 

 我们在派生类中再写一个虚函数,派生类就能实例化出对象了。

 4.2接口继承和实现继承:

实现继承

普通函数的继承是实现继承,不是接口继承,继承的是函数的实现,可以直接使用这个函数,也是一种复用。

接口继承 

虚函数包括纯虚函数的继承是接口继承,子类仅仅只继承了父类接口 ,父类没有实现这个接口函数,子类要对纯虚函数进行重写达到多态的目的。

5.多态的原理

5.1虚函数表 

我们计算一下sizeof(a)

class Father 
{
public:
	virtual void func()
	{

	}
	int car;
};

int main()
{
	Father a;
	cout << "sizeof(a): " << sizeof(a) << endl;
	
}

就一个变量 car,所以是4。 

但是我们看看结果。 


没有调查就没有发言权!我们查一下内存再说话。

 我们就创建一个变量car,但是上面还有一个变量(很明显是个指针)。

对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

为了方便观察,我们改进一下代码

class Father
{
public:
	virtual void func1()
	{
		cout << "func1" << endl;
	}
	virtual void func2()
	{
		cout << "func2" << endl;
	}
	 void func3()
	{
		cout << "func3" << endl;
	}
	int car=1;
};
class Son :public Father
{
public:
	virtual void func2()   //让func2和father构成
	{}
	int phone = 2;
};

int main()
{
	Father a;
	cout << "sizeof(a): " << sizeof(a) << endl;
	Son  s;
}

 看一下监视窗口。

通过观察发现:

  1. 类对象son中也有一个虚表指针,son对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
  2. 父类对象和子类对象虚表是不一样的,func2完成了重写,所以b的虚表中存的是重写的Son::func2,所以虚函数的重写也叫作覆盖覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. fun1继承下来后是虚函数,所以放进了虚表,func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
  5. 总结一下派生类的虚表生成:

             a.先将基类中的虚表内容拷贝一份到派生类虚表中    

             b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的                     虚函数 

           c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

  • 虚函数存在哪的?虚表存在哪的?

       虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是它的指针又存到了虚表中另外对象中存的不是虚表,存的是虚表指针。虚表在vs下存在代码段里。
 

多态是如何实现当父类指针指向父类对象时,调用父类的虚函数,而父类指针指向子类对象时,调用的就是子类的虚函数呢? 接下来我们看看多态的原理。

5.2多态的原理 

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class Food  //基类
{
public:
	virtual void eatfood()   // 虚函数
	{
		cout << "eat food--基类" << endl;
	}
};

class Breakfast :public Food
{
public:
	virtual void eatfood()
		//void eatfood()  子类不需要加virtual也可以和父类的构成重写,但是不规范且有许多漏洞。
	{
		cout << "eat breakfast---派生" << endl;
	}
};

void func(Food& e)   //吃饭
{
	e.eatfood();
}
int main()
{
	Food f;
	func(f);
	Breakfast bf;
	func(bf);
}

 我们看,父类的对象f被e指向时,调用func函数的eatfood并且在虚数表中找到虚函数Food::eatfood.

子类的对象bf也是这样,但是eatfood会在虚数表中找到虚函数Breakfast::eatfood。

 如果子类没有重写父类的虚函数,此时就破坏了多态的形成条件,那此时和上面的情况会一样吗?

此时就不再是上面的情况了。

父类调用: 

我们不在用父类指针和父类引用了,我们用一下父类调用。

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;

class Food  //基类
{
public:
	virtual void eatfood()   // 虚函数
	{
		cout << "eat food--基类" << endl;
	}
};

class Breakfast :public Food
{
public:
	virtual void eatfood()   // 虚函数
	{
		cout << "eat food--子类" << endl;
	}
	 
};

void func(Food e)   //吃饭
{
	e.eatfood();
}
int main()
{
	Food a;
	func(a);
	Breakfast b;
	func(b);
}
  • 此时就不在构成多态了。我们用子类对象入参时,那么指针和引用会把父类那部分切出来,切出来后不是赋值,而是让指针指向子类里面父类的那部分,这个指针无论指向的是父类还是子类,看到的都是父类对象,给父类引用的就是父类对象,给子类引用的是切片出来的父类对象。
  • 而构成多态时,引用和指针本身并不知道自己指向或引用的是父类对象还是子类对象,指向父类对象,那就指向或引用整个父类对象,指向子类对象,那就那看到的就是子类对象中父类那一部分。 

5.3动态绑定和静态绑定

  • 静态绑定:又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载.
  • 动态绑定:又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

6.单继承和多继承关系的虚函数表 

6.1单继承中的虚函数表

虚函数表初始化分为3步:

  1. 开始执行基类的构造函数
  2. 初始化基类的成员
  3. 将基类构造函数执行完毕

发现执行完以上3步之后,虚表指针已经初始化了: 

虚表指针是在构造函数初始化列表阶段初始化的,虚表在编译时就已经生成了。

一个类中所有的虚函数地址,都会放到虚表中。虚表里面存放的是虚函数地址,虚函数和普通函数一样, 编译完成后,都放在代码段。

我们看一下派生类虚表生出过程

我们用上面,funct1,func2, func3那个代码,我就不写这上面了。

父类的虚表中存的是Aniaml的func1( )和func2( )的地址。生成子类虚表时,会单独开辟一块空间,拷贝一份父类虚表过程中,会将对应虚函数位置覆盖成子类重写了父类的虚函数,如果子类没有重写,那么父类的虚函数就不会被覆盖,保留。所以子类虚表的生成过程是一个拷贝+覆盖的过程。

监视如上代码:

(1)子类重写了父类的func2( )虚函数,所以子类会覆盖父类func2( )位置;

(2)子类没有重写父类的func1( )虚函数,子类不会覆盖父类func1( )位置;

(3)父类的Jump( )不是虚函数, 不会出现在虚表中:

 再看一下他们的地址变化。

6.2多继承的虚表变量 

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
int main()
{
	Derive d;
	return 0;
}

 

 这跟单继承的虚表如出一辙。

我们打印一下虚表:

ypedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Derive d;
	PrintVTable((VFPTR*)(*(int*)&d));
	PrintVTable((VFPTR*)(*(int*)((char*)&d + sizeof(Base1))));
	return 0;
}

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。 

  • 1.假设D类先继承B1,然后继承B2,B1和B2基类均包含虚函数,D类对B1和B2基类的虚函数重写了,并且D类增加了新的虚函数,则D类有几个虚表?D类的虚函数放在那个虚表后面?答案 2,第一个

继承几个父类就有几个虚数表d自己的虚函数放在第一个虚数表后面其他父类的虚表不需要存储,因为存储了也不能调用。

  • 2.父类和子类各自用各自的虚表。
  • 3.虚表实在编译期间生成的。
  • 4.编译时多态是早期绑定,主要通过重载实现运行时多态注主要通过模板和虚函数实现。

 

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

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

相关文章

Java多线程(4):ThreadLocal

您好&#xff0c;我是湘王&#xff0c;这是我的CSDN博客&#xff0c;欢迎您来&#xff0c;欢迎您再来&#xff5e; 为了提高CPU的利用率&#xff0c;工程师们创造了多线程。但是线程们说&#xff1a;要有光&#xff01;&#xff08;为了减少线程创建&#xff08;T1启动&#xf…

Synchronized底层核心原理

前言&#xff1a;大家好&#xff0c;我是小威&#xff0c;24届毕业生&#xff0c;在一家满意的公司实习。本篇文章是关于并发编程中Synchronized锁的底层核心原理知识记录&#xff0c;由于篇幅原因&#xff0c;下篇文章将介绍各种锁的优化原理。 本篇文章记录的基础知识&#x…

vue3 异步组件

前端开发经常遇到异步的问题&#xff0c;请求函数&#xff0c;链接库&#xff0c;等&#xff0c;都有可能需要通过promise或者async await 来进行异步的一个封装。 异步组件也由此诞生&#xff0c;我用settimeout来模拟一个vue3的异步组件 异步的子组件 <template><…

spring框架源码十三、spring ioc高级特性-后置处理器

spring ioc高级特性-后置处理器BeanPostProcessor实例MyBeanPostProcessorapplication-context.xmlTestServiceImpl测试BeanFactoryPostProcessorspring提供了两种后置处理bean的扩展接口&#xff0c; 分别为BeanPostProcessor和BeanFactoryPostProcessor&#xff0c; BeanPos…

攻防世界WEB练习 | easyphp

目录 题目场景 代码分析 找到flag 题目场景 代码分析 if(isset($a) && intval($a) > 6000000 && strlen($a) < 3) isset&#xff1a;检查变量是否设置 intval&#xff1a;检查变量是否为int型 strlen&#xff1a;检查变量的长度 要求a存在且大于6…

Matlab之多平台雷达检测融合仿真(附源码)

此示例演示如何融合来自多平台雷达网络的雷达检测。该网络包括两个机载和一个地面远程雷达平台。中央跟踪器以固定的更新间隔处理来自所有平台的检测。这能够根据目标类型、平台机动以及平台配置和位置评估网络的性能。 一、定义中央跟踪器 将trackerGNN用作中央跟踪器&#…

云原生时代下,如何打造开源监控体系?宏时数据在GOPS与你相聚

相聚上海 宏时数据受邀出席2022 GOPS全球运维大会上海站&#xff0c;将分享演讲&#xff01; 时间&#xff1a;2022年10月28日15:20-15:40 AIOps最佳实践及解决方案专场 同时展位在301&#xff0c;现场有丰富礼品&#xff0c;快来做任务夺宝&#xff01; 还有Zabbix高级认…

【CSDN开发云】光速认识Cloud IDE

⌚️⌚️⌚️个人格言&#xff1a;时间是亳不留情的&#xff0c;它真使人在自己制造的镜子里照见自己的真相! &#x1f4d6;Git专栏&#xff1a;&#x1f4d1;Git篇&#x1f525;&#x1f525;&#x1f525; &#x1f449;&#x1f449;&#x1f449;你的一键三连是对我的最大支…

10.26 要尝试让自己安静下来,去做该做的事 而不是让内心烦躁,焦虑,毁掉你本就不多的热情和定力

要尝试让自己安静下来&#xff0c;去做该做的事 而不是让内心烦躁&#xff0c;焦虑&#xff0c;毁掉你本就不多的热情和定力 复习 import torch import torch.nn as nn import math from torch.autograd import Variable# 定义embedding类来实现文本嵌入层&#xff0c;这里的s…

C++多态详解及代码示例

多态 一、基本定义 顾名思义&#xff0c;多种形态。多态是C面向对象的三大特性之一&#xff08;封装、继承和多态&#xff09;。 多态分为两种&#xff1a; 静态多态&#xff1a;函数的重载、运算符的重载动态多态&#xff1a;派生类和虚函数实现运行时多态 区别&#xff…

基于javaweb的企业员工绩效工资管理系统(java+springboot+freemarker+mysql)

基于javaweb的企业员工绩效工资管理系统(javaspringbootfreemarkermysql) 运行环境 Java≥8、MySQL≥5.7 开发工具 eclipse/idea/myeclipse/sts等均可配置运行 适用 课程设计&#xff0c;大作业&#xff0c;毕业设计&#xff0c;项目练习&#xff0c;学习演示等 功能说明…

cadence SPB17.4 - allegro - DRC检查的细节

文章目录cadence SPB17.4 - allegro - DRC检查的细节概述笔记设置约束管理器设置modeDRC检查查看report查看status总结ENDcadence SPB17.4 - allegro - DRC检查的细节 概述 一个板子做完了, 打样回来, 找出一些小问题, 需要改下板子. 将铺铜拆了, 按照原理图补上元件. 将线都…

Qt实现桌面画线、标记,流畅绘制,支持鼠标和多点触控绘制

前言 经常会在网上直播讲课或者点评中看到可以在课件上或者桌面上进行画线标记划重点&#xff0c;其实实现并不难&#xff0c;原理就是在桌面上盖一个透明图层&#xff0c;然后根据鼠标点绘制曲线。 今天分享如何通过Qt的QGraphics体系来实现这个功能&#xff0c;以前的文章已…

23、STM32——CAN

1、CAN 协议简介 CAN 与 I2C、SPI 等具有时钟信号的同步通讯方式不同&#xff0c;CAN 通讯并不是以时钟信号来进行同步的&#xff0c;它是一种异步通讯&#xff0c;只具有 CAN_High 和 CAN_Low 两条信号线&#xff0c;共同构成一组差分信号线&#xff0c;以差分信号的形式进行通…

第31讲:MySQL事务的并发问题以及事务的隔离级别

文章目录1.事务的并发问题1.1.事务并发之脏读1.2.事务并发之不可重复读1.3.事务并发之幻读2.事务的隔离级别3.模拟事务并发问题的产生以及如何避免3.1.事务并发问题脏读的模拟以及避免3.1.1.模拟事务并发脏读的问题3.1.2.解决事务并发脏读的问题3.2.事务并发问题不可重复读的模…

MATLAB函数mesh与surf等绘制三维曲面入门

一、引言 三维曲面在实际应用中被广泛使用&#xff0c;能够更好的展示三维空间中曲面&#xff0c;以实现三维数据的可视化。 Matlab软件中可以使用mesh、fmesh、surf和fsurf等函数来实现三维曲面的绘图。其中mesh和fmesh用来绘制三维网格曲面图&#xff0c;surf和fsurf绘制三维…

使用OpenCV如何确定一个对象的方向

在本教程中&#xff0c;我们将构建一个程序&#xff0c;该程序可以使用流行的计算机视觉库 OpenCV 确定对象的方向&#xff08;即以度为单位的旋转角度&#xff09;。 最常见的现实世界用例之一是当您想要开发机械臂的取放系统时。确定一个物体在传送带上的方向是确定合适的抓…

Activiti工作流引擎中责任链模式的建立与应用原理

本文需要一定责任链模式的基础与Activiti工作流知识&#xff0c;主要分成三部分讲解&#xff1a; 一、简单理解责任链模式概念 网上关于责任链模式的介绍很多&#xff0c;菜鸟教程上是这样说的&#xff1a;责任链模式&#xff08;Chain of Responsibility Pattern&#xff09…

操作系统实验二 进程创建

百年传承的实验&#xff0c;看不懂题意就对啦 vim写C代码的时候&#xff0c;记得先insetr键&#xff0c;Esc键后:wq保存。 更改后记得gcc重新编译。 代码显示异常&#xff0c;看评论区。 《操作系统》实验报告 姓名 Rhyme_7 学号 1008611 实验序号 实验二 实验名称 实验…

概率论与数理统计学习:数字特征(一)——知识总结与C语言实现案例

hello&#xff0c;大家好 这里是第十期的概率论与数理统计的学习&#xff0c;我将用这篇博客去总结知识点和用C语言实现案例的过程。 本期知识点——期望 离散型随机变量的期望连续型随机变量的期望随机变量函数的期望期望的性质 &#x1f4a6; 期望的引入 随机变量的分布函…