总结虚函数表机制——c++多态底层原理

        前言: 前几天学了多态。 然后过去几天一直在测试多态的底层与机制。今天将多态的机制以及它的本质分享给受多态性质困扰的友友们。

        本节内容只涉及多态的原理, 也就是那张虚表的规则,有点偏向底层。 本节不谈语法!不谈语法!不谈语法!想要学习语法的话本节并不合适。

虚函数表

        一个类中, 如果包含虚函数成员, 那么这个类进行实例化的对象中,会出现一张虚函数表 

这个概念很重要,只要包含虚函数成员, 就有虚函数表!

                            只要包含虚函数成员, 就有虚函数表!

                            只要包含虚函数成员, 就有虚函数表!

通过调试下图代码进行观察:

class A
{
public:
	A()
	{
		func();
		cout << "A()" << endl;
	}
	virtual void func()
	{
		cout << "Afunc()" << endl;
	}
	void test()
	{
		func();
	}


private:
	int a = 0x11;//这里初始化使用十六进制是为了好观察
};

int main() 
{
	A a;

	return 0;
}

 

绿框框就是A类型的那张虚函数表。 注意:A类的任何实例化对象的虚函数表是同一个或着多个。

虚函数表如何生成

        虚函数表的本质是一个函数指针数组。 他被存放在常量区

编译器通过编译, 就能分析整个类中的函数成员是否存在虚函数。 从而向常量区申请一块内存用来存放虚函数地址的地址, 这也就是我们所说的虚函数表。所以, 虚函数表是在编译期间生成的。

        虚函数表是存放函数指针的函数指针数组,所以这张虚函数表的类型是函数指针的指针也就是二级指针, 然后我们类实例化对象要保存这么一张虚函数表就需要用到一个三级指针指向这张虚函数表。 是不是感觉很恶心?不过没关系, 这一切都是编译器的工作, 我们只需要了解原理即可。像上图中的A类的实例化对象a, 我们现在观察下它的内存:

ps: 因为不小心将对象a和成员变量名重复了, 下面将会令成员变量名变成_a;

        好了, 现在我们知道了虚函数表是在什么时候生成的。 那么虚函数表的指针_vfptr是什么时候加入到对象的成员变量中的呢?下面我们进行进行观察。 

        现在对象a刚刚定义。虽然成员变量还没有初始化,但是对象a中已经有了虚函数表的指针_vfptr, 只是没有初始化。 所以, 我们就可以下结论:如果一个类含有虚函数表, 那么这个类的实例化对象,会在生成的一瞬间增加虚函数表指针变量。但是这一个或者多个虚函数指针变量并没有进行初始化。也就是说, 如果类中含有虚函数, 那么这个类中默认存在一个_vfptr指针变量。(这里可能增加多个虚函数表指针, 这个涉及继承, 也是多态的本质。后续讨论)。

        然后我们接着进行调试, 观察这个虚函数表指针什么时候进行初始化 

进入构造函数, 没有变化。 继续调试。

        变了。 当我们调试到初始化列表的时候, 对象a的虚函数表进行了初始化。 这里第二个结论就出来了:对象的虚函数表会在构造函数的初始化列表进行初始化。也就是说, _vfptr指针和其他的成员变量一样, 都是在初始化列表进行初始化。

通过上面的分析,我们基本可以总结一下:虚函数列表是在编译期间进行生成的。 它存放在常量区而虚函数列表的指针_vfptr会和类的其他成员变量一样。 实例化瞬间声明变量, 初始化列表初始化变量。

继承与重写

        多态在语法规则上, 说了多态的形成条件:1、虚函数的重写;

                                                                             2、父类对子类的引用或者父类的指针指向子类。

重写

        现在, 我们来讨论虚函数的重写:

        虚函数在什么情况下会有重写的概念?当一个父类存在虚函数。 并且子类同样有函数名称相同的函数(这个时候不管这个函数是不是虚函数, 都会被编译器默认识别成虚函数)的时候就会有重写的概念。这里的虚函数名称相同是指函数名称相同,函数参数相同,返回值相同。  

        重写必须有相同函数名称的虚函数的继承。继承后子类的虚函数列表之中不会再有父类的相应的虚函数。只存在重写的虚函数。但是不影响父类。 

        如图:


class A
{
public:

	virtual void func(int a = 1)
	{
		cout << "Afunc():" << a << endl;
	}


private:
	int _a = 0x11;//这里初始化使用十六进制是为了好观察
};

class C : public A
{
public:

	virtual void func(int a = 3)//该func重写了父类的func. 所以C类的虚函数列表之中不会包含父类的func,只有自己这个重写了的func。
	{
		cout << "Cfunc()" << a << endl;
	}


	int c = 0x11;
};

int main()
{
	C cc;
	A* ptr = &cc;
	ptr->func();
	cc.func();
	return 0;
}

        这里子类的func重写了父类的func. 所以C类的虚函数列表之中不会包含父类的func,只有自己这个重写了的func。(这条重写性质非常重要! 后续多态的形成会用到!)

我们观察一下vs:

        通过观察右边的内存窗口,我们可以看到C的实例化对象cc之中的虚函数表中只有一个 函数的地址。这里的00 00 00 00就是虚函数列表结尾的标志。 相当于字符串的末尾斜杠零)。

        

虚函数表的继承

       但是图中, 现在我们还可以看到另外一个现象。 那就是为什么cc的虚函数指针跑到了继承的A类的虚函数列表里?

        这里是虚函数列表继承的规则。

         如果是单继承。那么子类的虚函数的地址和继承来的父类的虚函数的地址都会存放在一张虚函数列表之中。并且这张虚函数列表之中父类的虚函数在低地址。 子类的虚函数在高地址。

        如下是一个单继承。


class A
{
public:

	virtual void func(int a = 1)
	{
		cout << "Afunc():" << a << endl;
	}


private:
	int _a = 0x11;//这里初始化使用十六进制是为了好观察
};

class C : public A
{
public:

	virtual void func(int a = 3)
	{
		cout << "Cfunc()" << a << endl;
	}


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

	int c = 0x11;
};


int main()
{
	C cc;
	A* ptr = &cc;
	ptr->func();
	cc.func();
	return 0;
}

        在C类中, C只继承了A类, 所以是单继承。  C类之中只有一张虚函数列表。 这张虚函数列表之中包含了C重写的虚函数func()和自己本身的虚函数func2().我们看一下内存图:

        这里需要注意的是不要只观察监视窗口绿色箭头所指向的地方。 因为监视窗口有些时候是不准的, 就像这个时候就不准。 我们需要看一下内存窗口红色箭头指向的地方。 这里是真正的底层内存。 我们可以发现C类的实例化对象中只有一张虚函数列表, 并且这张虚函数列表之中有两个函数指针。 一个是重写的父类的func函数指针。 一个是自己的func2函数指针。 (其实这里我总结了一个结论:任意的虚函数表之中只有三类虚函数指针,第一种是继承的父类的并且重写了的虚函数指针, 第二种是继承的父类的没有被重写的虚函数指针, 第三种是自己的虚函数指针。C类虚函数表中包含的指针式第一种和第三种)

        现在看多继承。

        如果是多继承,假如继承了n个父类。那么子类会有n张虚函数表。 子类的虚函数地址和第一个继承的类的虚函数地址存放在一张虚函数表之中,地址存放规则同单继承。其他虚函数表各自存放继承来的父类的虚函数或者重写的虚函数。   这里要注意的是, 子类对象的这些虚函数表与父类的虚函数列表不是同一张, 但是如果虚函数没有被子类重写, 那么里面存放的虚函数地址是相同的。

这里测试如下代码


class A
{
public:

	virtual void func(int a = 1)
	{
		cout << "Afunc():" << a << endl;
	}
	virtual void funcA() 
	{
		cout << "func()A" << endl;
	}


private:
	int _a = 0x33;//这里初始化使用十六进制是为了好观察
};

class B
{
public:

	virtual void funcB()
	{
		cout << "func()B" << endl;
	}

private:
	int b = 0x22;
};

class C : public A, public B
{
public:

	virtual void func(int a = 3)
	{
		cout << "Cfunc()" << a << endl;
	}


	virtual void funcC()
	{
		cout << "func()C" << endl;
	}

	int c = 0x11;
};


int main()
{
	C cc;
	A* ptr = &cc;
	ptr->func();
	cc.func();
	return 0;
}

如图是C类继承A类和B类之后的实例化对象cc中的虚函数表情况。 可以看到,cc中有两张虚函数表。 一张存放在A类的板块, 一张存放在B类的板块。(这里为什么要分板块, 是因为要切片。 当我们使用父类的引用引用子类对象或者父类指针解引用子类对象的时候,我们能拿到的就是相应的为继承来的变量开辟的板块空间) 

现在我们就来观察底层内存空间。

                  

        这时C, B, A类的成员变量。 我们通过对他们进行缺省值初始化。 为了将他们每个板块区分出来。 现在我们开始观察空间。 

        对cc取地址, 观察cc的空间

 

这里红色箭头就是子类C本身的变量_c, 绿色箭头就是继承的B类的变量_b, 蓝色箭头就是继承的A类的变量_a。

所以这里我们就能判断, A类的内存板块就是前八个字节。B类的内存板块是中间八个字节。然后最后四个字节是C类本身的成员变量c的内存空间。


多态的形成

        现在我们来重新回顾一下语法上多态的形成条件:

        1、虚函数的重写

        2、父类对子类的引用或者父类的指针对子类的解引用

        现在虚函数的重写上面我们已经进行了分析。 现在我们来分析第二条内容。

        当我们进行父类对子类的引用或者父类的指针对子类进行解引用的时候, 这时候其实会发生切片原理。

        如图利用A*的ptr指针解引用一个C类的实例化对象相当于只拿到了图中横线上面的部分。

        我们已经知道, ptr解引用后拿到的其实是C类实例化对象之中属于A类的那一块内存。这块内存中的虚函数表之中的虚函数就是继承而来的A类的虚函数。 (如果有函数被重写, 还要将相应的虚函数进行替换。 如果忘记了这条性质, 请回看重写模块)

        这个时候如果, C类中继承A类而来的虚函数被重写时,并且我们恰好通过ptr调用了C这个被重写的虚函数, 这就是多态。
        也就是说我们使用A类的指针找到了C的实例化对象中的重写的继承A类而来的虚函数。这个结果其实和我们使用C类的实例化对象调用相应的虚函数的结果是一样的。

        这里重点就是切片原理和重写的性质。

最后还有另外一个要点:就是对于缺省值。

        对于多态的缺省值, 我们要特殊关照。

如果构成了多态, 那么这个时候调用的虚函数的缺省值应该是父类的缺省值。

        对于如下代码进行测试



class A
{
public:

	virtual void func(int a = 1)
	{
		cout << "Afunc():" << a << endl;
	}
	virtual void funcA() 
	{
		cout << "func()A" << endl;
	}


private:
	int _a = 0x33;//这里初始化使用十六进制是为了好观察
};


class C : public A
{
public:

	virtual void func(int a = 3)
	{
		cout << "Cfunc()" << a << endl;
	}


	virtual void funcC()
	{
		cout << "func()C" << endl;
	}

	int c = 0x11;
};


int main()
{
	C cc;
	A* ptr = &cc;
	ptr->func();
	return 0;
}

这是A类中的func

这是C类中的func

 请问。 测试中的代码, 打印结果是什么呢?

我们看一下vs的结果:

这里之所以不是Cfunc()3的原因是因为这里的缺省值使用的时父类的缺省值。  

        这里我们需要特殊记忆, 当构成多态的时候。 假如虚函数有缺省值, 那么这个缺省值时是父类的缺省值。 如果父没有缺省值, 子类有。 那么这个重写的虚函数没有缺省值。 注意, 这里是当构成多态的时候 !当构成多态的时候!当构成多态的时候!假如没有构成多态, 我直接使用C类对象调用重写的func函数, 那么结果就还剩Cfunc()3.

以上, 就是多态的底层原理。 

        

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

每日一题|djwcb【算法赛】|字符串快速幂

每日一题|djwcb【算法赛】 djwcb 心有猛虎&#xff0c;细嗅蔷薇。你好朋友&#xff0c;这里是锅巴的C\C学习笔记&#xff0c;常言道&#xff0c;不积跬步无以至千里&#xff0c;希望有朝一日我们积累的滴水可以击穿顽石。 djwcb 注意&#xff1a; 快速幂字符串&#xff0c;看…

js获取cookie

js获取cookie 前言实现讲解特别注意&#xff1a; 前言 主要是通过document.cookie来进行实现的 实现讲解 首先通过document.cookie 来获取到所有的cookie 然后通过分号进行分割成list 然后循环list,将list中的字符串通过首个等号进行分割然后和指定的cookie名进行比对然后返…

Android 导航方式切换

1.导航栏样式目前有&#xff0c;三键导航&#xff0c;也有全局手势导航&#xff0c;具体的设置是在setting里 setting里对应的 代码逻辑在packages\apps\Settings\src\com\android\settings\gestures\SystemNavigationPreferenceController.java static boolean isOverlayPacka…

芯片设计工程师必备基本功——《Verilog+HDL应用程序设计实例精讲》

进入芯片行业需要学习哪些基本功呢&#xff1f;其实芯片设计工程师的技能是通过多年的经验学习的。在您开始作为芯片设计工程师工作之前&#xff0c;很难给出一个需要的全面的单一列表&#xff0c;也不可能学习所有内容。话虽如此&#xff0c;但您开始芯片设计师职业生涯时必须…

图论基础|417. 太平洋大西洋水流问题、827.最大人工岛、127. 单词接龙

目录 417. 太平洋大西洋水流问题 827.最大人工岛 127. 单词接龙 417. 太平洋大西洋水流问题 题目链接(opens new window) 有一个 m n 的矩形岛屿&#xff0c;与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界&#xff0c;而 “大西洋” 处于大陆的右边界…

EDR下的线程安全

文章目录 前记进程断链回调执行纤程内存属性修改early birdMapping后记reference 前记 触发EDR远程线程扫描关键api&#xff1a;createprocess、createremotethread、void&#xff08;指针&#xff09;、createthread 为了更加的opsec&#xff0c;尽量采取别的方式执行恶意代…

C语言---------strlen的使用和模拟实现

字符串是以‘\0’作为结束标志&#xff0c;strlen函数的返回值是‘\0’前面的字符串的个数&#xff08;不包括‘\0’&#xff09; 注意 1&#xff0c;参数指向的字符串必须以‘\0’结束 2&#xff0c;函数的返回值必须以size_t,是无符号的 使用代码 ​ #include<stdio.…

数据库备份和恢复

前期准备 至少准备两台虚拟机完成Mysql的相关操作&#xff01; 在生产环境中&#xff0c;数据的安全性至关重要&#xff0c;任何数据的丢失都可能产生严重的后果。 造成数据丢失的原因&#xff1a; 程序错误人为操作错误运算错误磁盘故障&#xff1a; 解决措施&#xff1a;rai…

eric7安装报错

如果你遇到这样的问题&#xff1a; Sorry, please install QScintilla2 and its PyQt6 wrapper. Error: DLL load failed while importing Qsci: 找不到指定的程序。 如果你遇到这样的回答pip install QScintilla 如果你执行了这样的命令后&#xff0c;依旧报上面的错&#xf…

Word邮件合并

Word邮件合并功能可以解决在Word中批量填写内容的需求&#xff0c;当需要大量格式相同&#xff0c;只修改少数相关内容时&#xff0c;例如利用Word制作工资条&#xff0c;通知函&#xff0c;奖状等等&#xff0c;同时操作也非常简单灵活。下面通过例子来说明邮件合并的使用方法…

隧道技术和代理技术(四)SSHDNSICMPSMB上线通讯

目录 SSH&DNS&ICMP&SMB&上线通讯 DNS 隧道上线 DNS 隧道通讯 SSH 隧道通讯 SSH 隧道上线 SSH&DNS&ICMP&SMB&上线通讯 -连接方向&#xff1a;正向&反向&#xff08;基础课程有讲过&#xff09; -内网穿透&#xff1a;解决网络控制上…

《数据安全技术 数据分类分级规则》及典型行业标准指南要点提炼

数据分类分级发布新国标 千呼万唤&#xff0c;国家标准GB/T 43697-2024《数据安全技术 数据分类分级规则》于3月21日正式发布。作为全国网络安全标准化技术委员会更名后&#xff0c;发布的第一部以“数据安全技术”命名的国家标准&#xff0c;《数据安全技术 数据分类分级规则…

C++内存分布知识点复习

C内存分布&#xff08;只是用来自己复习&#xff0c;如果侵权了请联系&#xff0c;马上删除&#xff09; 看看自己是否了解 #define _CRT_SECURE_NO_WARNINGS #include<malloc.h> int globalVar 1; static int staticGlobalVar 1; void Test() {static int staticVa…

优秀人才必须具备的5个基本素质?

哈喽&#xff0c;大家好啊&#xff0c;我是雷工&#xff01; 张一鸣曾经在其微博上说过这么一句话&#xff1a; 选择越高级影响越大的人才&#xff0c;越要看一些基本素质。 那么优秀人才都具备哪些较好的基本素质呢&#xff1f; 或者说要具备哪些基本素质才有望成为高级人才&a…

[文生音乐]Suno AI:一句话,创作自己的音乐,全民创作人时代!

前言&#xff1a; 今年继AI生成文字&#xff08;Claude3&#xff09;、AI生成图片&#xff08;Midjourney&#xff09;、AI生成视频&#xff08;Sora&#xff09;之后&#xff0c;国外的AI公司又推出了一个劲爆的工具&#xff1a;Suno AI&#xff0c;通过一段简短的描述&#…

4G/5G视频记录仪_联发科MTK6765平台智能记录仪方案

视频记录仪主板采用了联发科MT6765芯片&#xff0c;该芯片采用12nm FinFET制程工艺&#xff0c;8*Cortex-A53架构&#xff0c;搭载安卓11.0/13.0系统&#xff0c;主频最高达2.3GHz&#xff0c;待机功耗可低至5ma&#xff0c;并具有快速数据传输能力。配备了2.4英寸高清触摸显示…

解决 cv2.imread读取带中文路径图片问题

http://t.csdnimg.cn/i8CXn 1.问题&#xff1a; # 中草药数据集样本可视化展示 import cv2 import matplotlib.pyplot as plt %matplotlib inline plt.title("heshouwu") plt.imshow(cv2.imread(r"D:\home\aistudio\data1\archive\train\何首乌\heshouwu_0001.…

huggingface的transformers训练bert

目录 理论 实践 理论 https://arxiv.org/abs/1810.04805 BERT&#xff08;Bidirectional Encoder Representations from Transformers&#xff09;是一种自然语言处理&#xff08;NLP&#xff09;模型&#xff0c;由Google在2018年提出。它是基于Transformer模型的预训练方法…

机器学习(一)

经典定义: 利用经验改善系统自身的性能。 经典的机器学习过程: 基本术语: 数据集:训练集、测试集 示例、样例、样本 属性、特征:属性值 属性空间、样本空间、输入空间 特征向量 标记空间、输出空间 归纳偏好(偏置): 任何一个有效的机器学习算法必有其偏好 学习算法的…

ConcurrentHashMap 为什么是线程安全的?

1、典型回答 ConcurrentHashMap 在不同JDK 版本中&#xff0c;保证线程安全的手段是不同的&#xff0c;它主要分为以下两种情况&#xff1a; JDK 1.7 之前(包含JDK 1.7)&#xff0c;ConcurrentHashMap 主要是通过分段锁 (Segment Lock) 来保证线程安全的。而在JDK 1.8 之后(包…