【C++进阶】多态详解(下)

news2024/11/24 23:07:16

文章目录

  • 单继承中的虚函数表
  • 多继承中的虚函数表
  • 动态绑定与静态绑定
  • 问题探究
    • 第一步:观察普通调用的汇编代码
    • 第二步:观察ptr1的汇编代码:
    • 第三步:观察ptr2的汇编代码:
    • 总结:
  • 继承和多态常见的面试问题

单继承中的虚函数表

我们来通过一段单继承的程序来观察一下:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func5()
	{
		cout << "Derive::Func5()" << endl;
	}
private:
	int _d = 2;
};
void func(Base& p)
{
	p.Func1();
}

在这里插入图片描述
通过调试,我们发现在父类对象b的虚表中有两个虚函数指针,这个我们可以理解,但是子类虚表也只有两个,分别是重写的fun1,还有继承下来的fun2,那么子类的虚函数fun5去哪里了呢,先俩通过内存观察一下:
在这里插入图片描述
我们通过观察虚函数表,发现除了在调试阶段看到的两个 函数以外,还有一个函数指针存储到虚函数表里边。


到这里,还有人可能说内存中看到并不代表就是我们子类中的虚函数,当然也可以通过打印虚函数表来实现:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func5()
	{
		cout << "Derive::Func5()" << endl;
	}
private:
	int _d = 2;
};
void func(Base& p)
{
	p.Func1();
}

typedef void(*VFPTR)();

void PrintVFTable(VFPTR* table,size_t n)
{
	for (size_t i = 0; i < n; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		//table[i]();  
		VFPTR pf = table[i];
		pf();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
	PrintVFTable((VFPTR*)*(int*)&d,3);
	PrintVFTable((VFPTR*)*(int*)&b,2);
	return 0;
}

在这里插入图片描述
打印了虚函数表之后,结果就很明了了,但是这里在打印虚函数表传参时,可能还有点问题,这里要传入的参数为(VFPTR*)*(int*)&d,这是因为对d取地址之后,代表整个类的地址,我们强转为int*类型,只看前四个字节的地址,就是虚函数表的地址,将地址解引用,得到一种整形数字,将整形数字强转为函数指针类型,就能得到函数表中的函数指针。

多继承中的虚函数表

在前边继承的章节,我们学习到了多继承,多继承就是一个子类拥有两个或者两个以上的直接父类。

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;
};

在多继承中,当父类1和父类2 都有虚表时,子类会将父类的虚表都继承下来,所以说子类中有两个虚表,通过调试来观察一下:
在这里插入图片描述
在这里插入图片描述


此时,又出现了和单继承同样的情况,子类中的虚函数到底有没有存在虚表中,存在哪个虚表中呢?
先来看Base2的虚函数表:
在这里插入图片描述

再来看Base1的虚函数表:
在这里插入图片描述
其实子类中的虚函数是会存在第一个虚函数表中。


动态绑定与静态绑定

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

问题探究

在在观察调试时,发现了一个问题,在两个虚函数表中,子类重写的func1函数,指向同一个函数,那么为什么他们的地址不同呢?
在这里插入图片描述

int main()
{
	Derive d;
	d.b1 = 1;
	d.b2 = 2;
	d.d1 = 3;
	Base1* ptr1 = new Derive;
	Base2* ptr2= new Derive;
	ptr1->func1();
	ptr2->func1();
}

在这里插入图片描述
为了搞清楚这个问题,我们来观察汇编来解决这个问题:

第一步:观察普通调用的汇编代码

由于ptr3是普通调用,所以是直接跳转的:
在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

第二步:观察ptr1的汇编代码:

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

第三步:观察ptr2的汇编代码:

在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述


在这里插入图片描述

总结:

其实我们发现,在虚函数表中存的并不是函数真正的地址,只是一条跳转语句,只不过他们最后跳转的地方都是相同的函数,当然地址也是相同的。

继承和多态常见的面试问题

  1. 什么是多态?

多态就是同一操作作用于不同的对象,会产生不同的结果。

  1. 什么是重载、重写(覆盖)、重定义(隐藏)?

重载:就是函数重载,一个函数名相同,但是参数的类型不同,参数的顺序不同,参数的个数不同,这是因为函数名在修饰后是不同的。
重写:重写就是多态的一个条件,首先父类必须是虚函数,而且子类中的函数必须和父类的函数名,参数,返回值都必须相同。但是有两个特殊情况,在前边的文章中有详细介绍。
重定义:重定义就是父类和子类中的函数名相同,子类中的函数会对父类中的函数会进行隐藏,所以要访问父类中的函数,必须突破父类的类域。

  1. 多态的实现原理?

当子类的函数对父类函数完成重写时,将父类中的虚函数复制,并且修改重写的函数,当我们使用父类的指针和引用访问函数时,在运行时决议,指针指向父类调父类函数,指针指向子类调子类函数,所以就实现了多态。

  1. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
    inline,因为虚函数要放到虚表中去。
  2. 静态成员可以是虚函数吗?

答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表,没有this指针,但是要访问虚表必须使用this指针,所以他们是矛盾的。

  1. 构造函数可以是虚函数吗?

答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。

  1. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?

答:可以,并且最好把基类的析构函数定义成虚函数。在上篇文章中,有一种特殊情况,如果不将析构函数实现多态,会少析构子类中除去子类中的一部分。

  1. 对象访问普通函数快还是虚函数更快?

如果没有实现多态,虚函数和普通函数都是一样快的,如果实现了多态,那么虚函数更慢,因为多态调用是运行时决议的,必须在虚函数表中去寻找函数。

  1. 虚函数表是在什么阶段生成的,存在哪的?

答:虚函数表是在编译阶段就生成的,虚函数表是只读的,一般情况下存在代码段(常量区)的。但是切记虚函数表指针是存在对象中的。

  1. C++菱形继承的问题?虚继承的原理?答:参考继承课件。注意这里不要把虚函数表和虚基
    表搞混了。
  2. 什么是抽象类?抽象类的作用?答:抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

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

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

相关文章

Linux:shell之编程免交互

Linux&#xff1a;shell之编程免交互 一、Here Document 免交互1.1 Here Document 免交互概述1.2 语法格式1.3 操作 二、Expect 命令2.1 Expect 概述2.2 基本命令2.3 操作 一、Here Document 免交互 1.1 Here Document 免交互概述 使用I/O重定向的方式将命令列表提供给交互式…

ACM 1011 | 最大公约数与最小公倍数

文章目录 0x00 前言 0x01 题目描述 0x02 问题分析 0x03 代码设计 0x04 完整代码 0x05 运行效果 0x06 总结 0x00 前言 C 语言网不仅提供 C 语言&#xff0c;还包括 C 、 java 、算法与数据结构等课程在内的各种入门教程、视频录像、编程经验、编译器教程及软件下载、题解博…

2023上海市大学生网络安全大赛—ssql题解

Part1前言 上海市大学生网络安全大赛的一道 pwn 题目&#xff0c;题目用了双向链表&#xff08;猜到是 Unlink 漏洞&#xff09;。 还算比较简单&#xff0c;主要是分析代码比较复杂。分析完后漏洞限制条件少&#xff0c;题目给了 libc2.31&#xff0c;利用比较灵活。 这题白天…

linux【网络编程】TCP协议通信模拟实现、日志函数模拟、守护进程化、TCP协议通信流程、三次握手与四次挥手

linux【网络编程】TCP协议通信模拟实现、日志函数模拟、守护进程化、TCP协议通信流程 一、TCP通信简单模拟实现1.1 服务端实现1.1.1 接口认识1.1.1.1 listen&#xff1a;监听socket1.1.1.2 accept&#xff1a;获取连接 1.1.2 tcpServer.hpp1.1.3 tcpServer.cc 1.2 客户端实现1.…

软考知识点---08IP地址与域名地址

一、IP地址 &#xff08;一&#xff09;什么是IP地址&#xff1f; 连入互联网的计算机&#xff0c;每台计算机或者路由器都有一个由授权机构分配的号码&#xff0c;IP地址代表这一台计算机在网络中的地址 在同一个网络中IP地址是唯一的 IP&#xff08;IPV4&#xff09;地…

音频的各项指标

对于下面data和linesize的解释(参考下面3.4中的av_samples_alloc_array_and_samples函数说明)&#xff1a; 1&#xff09;data是通道的意思&#xff0c;例如双通道&#xff0c;data[0]代表左声道&#xff0c;data[1]代表右声道。 2&#xff09;linesize为采样个数的最大大小字…

ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用

文章目录 序章&#xff1a;PDF版下载第一章&#xff1a;Java后端开发1.需求分析1.1 项目分析1.2 开发计划1.3 风险评估1.4 需求增强1.5 需求转情景 2.生成代码2.1 解析文件2.2 数据结构2.3 算法策略2.4 异步处理 3.Bug修改3.1 逻辑错误3.2 性能问题3.3 资源泄露3.4 死锁问题3.5…

MySQL之存储过程和存储函数

1. 存储过程概念 能够将完成特定功能的SQL指令进行封装(SQL指令集)&#xff0c;编译之后存储在数据库服务器上&#xff0c;并且为之取一个名字&#xff0c;客户端可以通过名字直接调用这个SQL指令集&#xff0c;获取执行结果。 2. 存储过程优缺点 2.1 优点 &#xff08;1&am…

【SpringCloud】二、服务注册发现Eureka与负载均衡Ribbon

文章目录 一、Eureka1、服务提供者与消费者2、Eureka原理分析3、搭建Eureka4、服务注册5、模拟多服务实例启动6、服务的发现 二、Ribbon1、负载均衡的原理2、源码分析3、负载均衡策略4、饥饿加载 一、Eureka 1、服务提供者与消费者 服务提供者&#xff1a;一次业务中&#xf…

Elastic-Job原理

Elastic-Job作业类型创建任务并执行 &#xff1a;启动流程弹性分布式实现 Elastic-Job elastic-job&#xff08;quartz的扩展&#xff09;使用了quartz的调度机制&#xff0c;内部原理一致&#xff0c;使用注册中心(zookeeper)替换了quartz的jdbc数据存储方式&#xff0c;支持…

ubuntu22.04切换回Xorg使用flameshot截图的问题

在ubuntu20.04时使用flameshot一切正常. 升级到ubuntu22.04之后,发现flameshot不能使用快捷键区域截图了,这个就很不方便. 在网上找了一圈后先是修改文件 /etc/gdm3/custom.conf将里面的 #WaylandEnableflase改成 WaylandEnablefalse即配置为不使用Wayland.然后重启系统,后…

动态通讯录实现(C语言)

目录 前言&#xff1a; 一&#xff1a;单个节点的设计和主逻辑 结点设计 主逻辑 二&#xff1a;接口实现 (1)生成一个新的结点 (2)增加信息 (3)打印信息 (4)查找 (5)删除信息 (6)修改信息 (7)排序 插入排序 快速排序 (8)已有数据读取 (9)更新数据录入 三&…

C语言复习笔记3

1.标识符常量和宏函数&#xff08;宏函数是简单替换所以需要把括号加到位&#xff09; #include<stdio.h>#define MAX 1000//标识符常量 #define num 10 //#define SUM(X,Y) XY //不对 #define SUM(X,Y) ((X)(Y))int max(int a, int b) {return a>b?a:b; }int main(…

系列八、vue配置请求

一、vue2配置请求转发 config/index.js proxyTable配置后端的请求地址 proxyTable: {/: {target: "http://localhost:9000", // 后端服务器地址changeOrigin: true,pathRewrite: {^/: }} }, 注意事项&#xff1a;vue2中不像大多数教程里边讲的那样&#xff0c;直接…

Apache NiFi:实时数据流处理的可视化利器【上进小菜猪大数据系列】

上进小菜猪&#xff0c;沈工大软件工程专业&#xff0c;爱好敲代码&#xff0c;持续输出干货。欢迎订阅本专栏&#xff01; Apache NiFi是一个强大的、可扩展的开源数据流处理工具&#xff0c;广泛应用于大数据领域。本文将介绍Apache NiFi的核心概念和架构&#xff0c;并提供…

路由守卫的几种方式-M

vue的路由 Vue-router是Vue.js官方的路由插件。vue的单页面应用是基于路由和组件的&#xff0c;路由用于设定访问路径&#xff0c;并将路径和组件映射起来。传统的页面应用&#xff0c;是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中&#xff0c;则是路径之…

C# | KMeans聚类算法的实现,轻松将数据点分组成具有相似特征的簇

C# KMeans聚类算法的实现 文章目录 C# KMeans聚类算法的实现前言示例代码实现思路测试结果结束语 前言 本章分享一下如何使用C#实现KMeans算法。在讲解代码前先清晰两个小问题&#xff1a; 什么是聚类? 聚类是将数据点根据其相似性分组的过程&#xff0c;它有很多的应用场景&…

章节1:信息收集

章节1:信息收集 1 信息收集概览 01 为什么要做信息收集&#xff1f; 渗透测试的流程 确定目标 信息收集 漏洞扫描 漏洞利用 形成报告 信息收集包括的内容 域名信息、IP段、开放的端口、网站架构、文件目录结构、软件版本、WAF、旁站、C段… 分类 域名相关信息IP相关…

Redis缓存数据库(四)

目录 一、概述 1、Redis Sentinel 1.1、docker配置Redis Sentinel环境 2、Redis存储方案 2.1、哈希链 2.2、哈希环 3、Redis分区(Partitioning) 4、Redis面试题 一、概述 1、Redis Sentinel Redis Sentinel为Redis提供了高可用解决方案。实际上这意味着使用Sentinel…

Java 与排序算法(1):冒泡排序

一、冒泡排序 冒泡排序&#xff08;Bubble Sort&#xff09;是一种简单的排序算法&#xff0c;它的基本思想是通过不断交换相邻两个元素的位置&#xff0c;使得较大的元素逐渐往后移动&#xff0c;直到最后一个元素为止。冒泡排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)&…