【1++的C++进阶】之多态

news2024/9/28 23:31:48

👍作者主页:进击的1++
🤩 专栏链接:【1++的C++进阶】

文章目录

  • 一,什么是多态?
  • 二,剖析多态的调用原理
  • 三,抽象类
  • 四,多继承中的虚函数表

一,什么是多态?

多态的定义:不同继承关系的类对象,去调用同一个函数,产生不同的行为。再说通俗点就是:一个行为,不同的对象去做会产生不同的结果。

构成多态的条件:

  1. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重写的条件:

  1. 是虚函数 (被virtual修饰的成员函数)
  2. 三同(函数名,参数,返回值)

特例:

  1. 子类虚函数不加virtual依旧构成重写。
  2. 重写的协变:返回值可以不同,但必须是父子关系的指针或引用
  3. 如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
    都与基类的析构函数构成重写。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

而且不符合重写就是隐藏关系,这也是我们判断隐藏关系的条件之一。
2. 必须是基类的指针或引用去调用虚函数。

实例如下:

class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
protected:
	int _a;
};

class B:public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	
	}
protected:
	int _b;
};
void Test1()
{
	B t1;
	A& a1 = t1;
	a1.Func1();
	A* ptra = &t1;
	ptra->Func1();
}

在这里插入图片描述

上面还遗留一个问题,为什么要把析构函数重写。

在这里插入图片描述
来看以下代码:

class A
{
public:

	virtual ~A()
	{
		cout << "~A()" << endl;
	}

	A& operator=(const A& a)
	{
		cout << "A::operator=()" << endl;
		return *this;
	}
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}
protected:
	int _a;
};

class B:public A
{
public:
	virtual ~B()
	{
		cout << "~B()" << endl;
	}

	/*B& operator=(const B& a)
	{
		A::operator=(*this);
		cout << "B::operator=()" << endl;
		return *this;
	}*/
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	
	}
protected:
	int _b;
};
void Test1()
{
	A* ptra1 = new A;
	A* ptra2 = new B;
	delete ptra1;
	delete ptra2;	
}

若析构函数没有重写:
在这里插入图片描述
我们可以看到new的B类类型的对象没有调用自己的析构而是其指针类的析构函数。这就会存在空间没有释放的问题,造成内存泄漏。

若构成重写后:
在这里插入图片描述
我们可以看到new出的B对象调用了自己的析构函数。并且上一篇文章我们讲过,子类对象的析构会先调用自己的构造函数,然后调用父类的构造函数。

这里我们做一个小的总结:通过上述的两段代码,我们会发现,多态调用重写函数,指向哪个对象就去调用哪个对象的重写函数。

二,剖析多态的调用原理

首先我们先来回答这么一个问题:

class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}

	virtual int Func2()
	{
		cout << "A::Func2()" << endl;
		return 0;
	}
protected:
	int _a;
};

class B:public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	
	}

protected:
	int _b;
};

void Test2()
{
	A t2;
	B t1;
	cout << sizeof(t1) << endl;

}

上述代码中的sizeof的值为多少呢?
在这里插入图片描述
答案为12!!可能会有人疑惑,为什么不是8呢?
我们通过监视窗口来观察。
在这里插入图片描述
我们发现,除了子类的成员变量和继承的A的成员变量外还多了一个_vfptr。这是虚函数表指针。
那么这个表中放的是什么呢?
我们继续来看下面这段代码:

class A
{
public:
	virtual void Func1()
	{
		cout << "A::Func1()" << endl;
	}

	virtual int Func2()
	{
		cout << "A::Func2()" << endl;
		return 0;
	}

	virtual int Func4()
	{
		cout << "A::Func4()" << endl;
		return 0;
	}
protected:
	int _a;
};

class B:public A
{
public:
	virtual void Func1()
	{
		cout << "B::Func1()" << endl;
	
	}

	virtual int Func3()
	{
		cout << "B::Func3()" << endl;
		return 0;

	}

	virtual int Func4()
	{
		cout << "B::Func4()" << endl;
		return 0;

	}
protected:
	int _b;
};



typedef void(*Vfptr)();

void PrintVFptr(Vfptr* arr)
{
	for (int i = 0; arr[i] != nullptr; ++i)
	{
		printf("vfptr[%d]->%p  ", i, arr[i]);
		Vfptr pf = arr[i];
		pf();
	}
}

void Test2()
{
	A t2;
	B t1;
	printf("B::vfptr\n");
	PrintVFptr((Vfptr*)*(int*)(&t1));
	printf("A::vfptr\n");
	PrintVFptr((Vfptr*)*(int*)(&t2));


}

在这里插入图片描述
在这里插入图片描述
再解读前,我们先来说明一个东西,只要是该类的虚函数,就会被存入该类的虚函数表中,并且对于单继承来说,每个类只有一份虚函数表,子类继承了父类的虚函数表,并且将重写的虚函数覆盖为自己的。也就是说子类的虚函数表是:继承父类的并进行重写覆盖后+自己的虚函数。

我们回来来解读上结果:Func1,Func4都进行了重写,所以我们发现其A与B打印出的函数地址不同。而Func2是通过继承下来的虚函数,但并没有进行重写,会存在虚表中,因此其在A和B的虚表中的函数指针相同。Func3则是子类中独有的虚函数,因此只在子类的需表中有。

所以多态的原理是:在编译阶段会形成虚函数表,在调用构造函数的初始化列表阶段会对虚函数表进行初始化。当程序运行后,在指向对象的虚函数表中去找对应的虚函数,这也是为什么我们前面说指向谁就调用谁,而对于普通函数来说,其在编译阶段就已经确定了调用谁。
在这里插入图片描述

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。

  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

三,抽象类

什么叫抽象类?
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

那么什么是接口继承呢?
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。
因此如果不实现多态,不要把函数定义成虚函数。

我们来看一道例题:

class A
 {
 public:
   virtual void func(int val = 1)
   { 
   		std::cout<<"A->"<< val <<std::endl;
   }

   virtual void test(){ func();}
 };
 
 class B : public A
 {
 public:
   void func(int val=0)
   { 
   		std::cout<<"B->"<< val <<std::endl; 
   }
 };
 
 int main(int argc ,char* argv[])
 {
   B*p = new B;
   p->test();
   return 0;
 }

问这道题的输出是什么?
在这里插入图片描述

答案是:B->1
这是为什么呢?
首先我们的我们用一个指向B对象的指针p去调用test函数,而test函数是父类中的虚函数,没有重写,会继承到子类B中,这时我们会忽略一个问题—this指针,test中的this指针是A类类型的指针,这符合构成多态的一个条件:父类指针或引用去调用虚函数。 我们将B*指针传过去后会发生切片,在test中调用func(),由于虚函数的继承是几口继承,因此其会继承父类的接口,用子类的实现。感觉像头和身子拼接起来的一样。因此,在这道题中会用到父类中func函数的缺省参数,而实现部分则用的是子类中的func。所以答案为:B->1。

四,多继承中的虚函数表

以以下代码为例:

class Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}

protected:
	int _a;

};

class Base2
{
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}

	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}

protected:
	int _b;

};

class Boss:public Base1,public Base2
{
public:
	virtual void func1()
	{
		cout << "Boos::func1" << endl;
	}

	virtual void func2()
	{
		cout << "Boos::func2" << endl;
	}

	virtual void func3()
	{
		cout << "Boos::func3" << endl;
	}

protected:
	int _c;

};

typedef void(*Vfptr)();

void PrintVFptr(Vfptr* arr)
{
	for (int i = 0; arr[i] != nullptr; ++i)
	{
		printf("vfptr[%d]->%p  ", i, arr[i]);
		Vfptr pf = arr[i];
		pf();
	}
}

int main()
{
	Base1 b1;
	Base2 b2;
	Boss d;
	cout << "Base1" << endl;
	PrintVFptr((Vfptr*)*(int*)(&b1));
	cout << "Base2" << endl;
	PrintVFptr((Vfptr*)*(int*)(&b2));
	cout << "Boos--1" << endl;
	PrintVFptr((Vfptr*)*(int*)(&d));
	cout << "Boos--2" << endl;
	PrintVFptr((Vfptr*)(*(int*)((char*)&d + sizeof(Base1))));

}

在这里插入图片描述
在这里插入图片描述
通过上述窗口和打印出的结果看,我们发现多继承其有n个虚函数表,n与继承的父类个数有关。并且,子类自身的虚函数放在第一个虚表中,如上述func3()。

还有一个有趣的现象:

Base1 b1;
	Base2 b2;
	Boss d;

	Base1* ptr1 = &d;
	Base2* ptr2 = &d;
	ptr1->func1();
	ptr2->func1();

在这里插入图片描述
若我们用子类的地址赋值给不同的父类指针去调用func1()按理来说,其两个指针都指向同一个子类对象,并且func1()都进行了重写,因此ptr1,ptr2 , Boos虚表中的func1()的地址应该是一样的,但是结果却不一样,这是为什么呢?
下面是d对象的模型:

在这里插入图片描述
因此当ptr2去调用func1()时,其传过去的this指针,并不是d的起始地址,因此为了使其this指针变为d的this 指针,这里编译器会进行一个操作,计算出Base1的大小,将ptr2减去Base1的大小,这时this指针就指向了d的起始位置,自然调用的就是d对象中的func1()。

补充两个关键字:

  1. final:修饰虚函数,表示该虚函数不能再被重写。
  2. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

在这里插入图片描述

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

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

相关文章

【LeetCode 75】第十九题(724)寻找数组的中心下标

目录 题目: 示例: ​分析: 代码运行结果: 题目: 示例: 分析: 给一个数组,让我们找出一个下标,在这个下标左边的元素总和等于这个下标右边的元素总和. 我们可以把整个数组的总和求出来,然后再从左往右遍历一次数组,遍历的同时将遍历过的数累加记录到一个变量中.若遍历到一…

【C语言进阶】数据的存储----整型篇

​ &#x1f341; 博客主页:江池俊的博客 &#x1f4ab;收录专栏&#xff1a;C语言——探索高效编程的基石 &#x1f4bb; 其他专栏&#xff1a;数据结构探索 ​&#x1f4a1;代码仓库&#xff1a;江池俊的代码仓库 &#x1f3aa; 社区&#xff1a;GeekHub &#x1f341; 如果觉…

数据库索引失效的情况

1.对添加了索引的字段进行函数运算 2.如果是字符串类型的字段&#xff0c;如果不加单引号也会导致索引失效 3.如果最索引字段使用模糊查询&#xff0c;如果是头部模糊索引将失效&#xff0c;如果是尾部模糊索引则正常 4.如果使用or分割符&#xff0c;如果or前面的条件中的列有…

JUC并发编程(二)ForkJoinPool、Future、CompletableFuture、CAS

文章目录 ForkJoin分治工作窃取ForkJoinPool与ThreadPoolExecutor使用案例不带返回值的计算--RecursiveAction带返回值的计算--RecursiveTask Future 异步回调烧水案例join实现FutureTask实现 CompletableFuture为什么叫CompletableFuture?创建异步任务supplyAsyncrunAsync获取…

Vue插槽 、自定义指令、render函数、过滤器和插件

目录 插槽 自定义指令 directive 全局注册 局部注册 钩子函数 render渲染函数 过滤器 插件 plugin 插槽 普通插槽&#xff0c;具名插槽&#xff0c;作用域插槽 插槽允许我们在调用子组件的时候为子组件传递模板。 <slot> 元素作为承载分发内容的出口。 一个不带…

双链表(带哨兵位头节点)

目录 ​编辑 双链表的初始化&#xff1a; 双链表的打印&#xff1a; 双链表的尾插&#xff1a; 双链表的头插&#xff1a; 双链表的尾删&#xff1a; 双链表的头删&#xff1a; 双链表pos位置之前的插入&#xff1a; 双链表pos位置的删除&#xff1a; 关于顺序表和链表…

MyBatis-Plus实现分页查询

目录 MyBatis-Plus实现分页查询 代码 定义一个MyBatis-Plus拦截器 在连接数据库的配置文件中添加MyBatis-Plus日志查看MyBatis-Plus的SQL语句 测试 运行结果 MyBatis-Plus实现分页查询 代码 定义一个MyBatis-Plus拦截器 package com.dong.config;import com.baomidou.my…

windows下mysql的下载与安装

文章目录 1 下载2 安装目录下新建data文件夹和my.ini3 安装4设置密码与远程连接5 配置环境变量6 navicate连接成功 1 下载 官网地址 https://www.mysql.com/点击下载 社区下载 社区服务 选择版本下载 2 安装目录下新建data文件夹和my.ini my.ini 内容如下 [mysql] # 设置my…

iphone内存不足导致白苹果?可以使用这2种办法解决!

因为iPhone内存不足没及时清理导致打开任何软件闪退&#xff0c;这时很多小伙伴会重启手机来解决闪退问题&#xff0c;但就会出现白苹果问题&#xff0c;无法正常进入手机系统、实现任何操作的一种状态。 内存不足导致iPhone白苹果的问题很常见&#xff0c;可以说是苹果最常见…

linux 文件的权限

修改文件的权限 我这里有一个test.txt 文件&#xff0c;我们ll 查看一下该文件相应的属性信息 其中&#xff0c;权限的位置是相对固定的即&#xff1a; 第一个位置是r 权限&#xff0c;代表可读权限。 第二个位置是w权限&#xff0c;代表可修改权限。 第三个位置是x权限&…

【腾讯云 Cloud Studio 实战训练营】从零开始搭建一个数据大屏

文章目录 前言得到什么?使用Cloud Studio登录Cloud Studio登录方式Cloud Studio 功能介绍项目创建配置描述新建工作空间绑定Coding创建仓库绑定coding创建项目项目空间 项目搭建nuxt 脚手架Cloud Studio 安装插件nuxt初始项目预览问题描述 下载Datav 并体验页面结构展示 获取基…

【项目 线程2】3.5 线程的分离 3.6线程取消 3.7线程属性

3.5 线程的分离 #include <stdio.h> #include <pthread.h> #include <string.h> #include <unistd.h>void * callback(void * arg) {printf("chid thread id : %ld\n", pthread_self());return NULL; }int main() {// 创建一个子线程pthread…

Vue3基础_响应式数据

setup是组合式API 选项式API&#xff0c;是data,methods,computed&#xff0c;watch等等全都是分开的&#xff0c;但是组合式API是把这些东西全都写在一起了。 1 vue2的缺点 (1)使用vue2 Vue2版本对数据的拦截用的是Object.defineProperty, 可以监测到对象的变化。因为o…

Spring源码篇(九)自动配置扫描class的原理

文章目录 前言ClassLoader如何加载jar包里的class自动配置扫描class的原理spring中的加载方式源码总结 前言 spring是怎样通过ComponentScan&#xff0c;或者自动配置扫描到了依赖包里class的&#xff1f; ClassLoader 这里涉及到了class Loader的机制&#xff0c;有些复杂&…

信必优行业服务能力-中国头部综合性证券公司

近期召开的国家高层会议提出 “要活跃资本市场&#xff0c;提振投资者信心”&#xff0c;明确了下一阶段资本市场发展新任务、新要求&#xff0c;资本市场有望呈现新气象、新风貌。各证券公司积极响应&#xff0c;全力推进资本市场回暖&#xff1b;同时各公司也借此东风修炼内功…

【科普知识】了解电机T型速度曲线和S型速度曲线的区别!

当电机从静止状态启动并加速到额定转速时&#xff0c;其速度变化并非线性的&#xff0c;而是呈现出不同的曲线特征。T型速度曲线和S型速度曲线是两种典型的电机加速曲线类型。那它们之间有什么区别呢&#xff1f;今天&#xff0c;就让我们来深入探讨电机加速曲线的奥秘。 电机速…

国内是不是很缺音视频的开发人员,想学习音视频开发

第一、音视频开发人员的培养是一个长期投入&#xff0c;见效慢的过程&#xff0c;不像有些培训机构&#xff0c;半年培训就可以出去找工作了。同时培训机构最终的目的是快速培训&#xff0c;推荐工作然后挣钱。而音视频开发见效太慢&#xff0c;没有一定时间的锻炼和项目喂养&a…

骨传导耳机什么牌子好?盘点最受欢迎的几款骨传导耳机

骨传导耳机最近一两年越来越受欢迎&#xff0c;市场上不同形态的非入耳式耳机都有&#xff0c;从骨传导&#xff0c;夹耳式到气传导等等都有。骨传导耳机的好处有很多&#xff0c;非入耳式&#xff0c;不伤耳朵&#xff0c;佩戴更舒适更安全。但是一直以来&#xff0c;骨传导耳…

如何把非1024的采样数放入aac编码器

一. aac对数据规格要求 二、代码实现 1.初始化 2.填入数据 3.取数据 三.图解 一. aac对放入的采样数要求 我们知道aac每次接受的字节数是固定的&#xff0c;在之前的文章里有介绍libfdk_aac音频采样数和编码字节数注意 它支持的采样数和编码字节数分别是&#xff1a; fdk_aac …

中规院:2023年黄河流域主要城市人居环境气象评估报告(附下载

关于报告的所有内容&#xff0c;公众【营销人星球】获取下载查看 核心观点 黄河流域年均高温天数呈上升趋势&#xff0c;近五年达到历史高位。 黄河流域年均高温天数由1978年的11日升至2022年的17日&#xff0c;整体呈上升趋势。1978-2022年间&#xff0c;2018年年均高温天数…