C++之深入解析虚函数表的实现及其内存布局

news2024/11/29 0:55:40

一、虚函数表概述

  • C++ 中的虚函数的作用主要是实现了多态的机制,关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); // Derive::f()
b2->f(); // Derive::f()
b3->f(); // Derive::f()
 
b1->g(); // Base1::g()
b2->g(); // Base2::g()
b3->g(); // Base3::g()
  • 这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI 技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。
  • 虚函数表是指在每个包含虚函数的类中都存在着一个函数地址的数组,当用父类的指针来操作一个子类的时候,这张虚函数表指明了实际所应该调用的函数。C++ 的编译器保证虚函数表的指针存在于对象实例中最前面的位置,这样通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。
  • 来看一个实际的例子,如下所示:
#include <iostream>

using namespace std;

class Base {
public:
	virtual void f() { cout << "f()" << endl; }
	virtual void g() { cout << "g()" << endl; }
	virtual void h() { cout << "h()" << endl; }
};

int main() {
	Base t;
	(     ((void(*)())*((int*)(*((int*)&t)) + 0))   )     ();
	(     ((void(*)())*((int*)(*((int*)&t)) + 1))   )     ();
	(	  ((void(*)())*((int*)(*((int*)&t)) + 2))	)     ();
	return 0;
}
  • 经过 VS2017,x86 测试:
f()
g()
h()

在这里插入图片描述

  • 可以看到,成功地通过实例对象的地址,得到了对象所有的类函数:

在这里插入图片描述

  • main 定义 Base 类对象 t,把 &b 转成 int ,取得虚函数表的地址 vtptr 就是:(int)(&t),然后再解引用并强转成 int * 得到第一个虚函数的地址,也就是 Base::f() 即 (int*)(((int)&t)),那么,第二个虚函数 g() 的地址就是 (int*)(((int)&t)) + 1,依次类推。

二、单继承下的虚函数表

① 派生类未覆盖基类虚函数

  • 现在来看下派生类没有覆盖基类虚函数的情况,其中 Base 类延用上一节的定义,从下图中可看出虚函数表中依照声明顺序先放基类的虚函数地址,再放派生类的虚函数地址:

在这里插入图片描述

  • 可以看到:
    • 虚函数按照其声明顺序放于表中;
    • 父类的虚函数在子类的虚函数前面。
  • 测试代码:
#include <iostream>

using namespace std;

class Base {
public:
	virtual void f() { cout << "f()" << endl; }
	virtual void g() { cout << "g()" << endl; }
	virtual void h() { cout << "h()" << endl; }
};

class Devired :public Base{
public:
	virtual void x() { cout << "x()" << endl; }
};

int main() {
	Devired t;
	(((void(*)())   *((int*)(*((int*)&t)))))   ();
	(((void(*)())*((int*)(*((int*)&t)) + 1)))  ();
	(((void(*)())*((int*)(*((int*)&t)) + 2)))  ();
	// (((void(*)())*((int*)(*((int*)&t)) + 3)))  ();

	return 0;
}
  • 测试效果:

在这里插入图片描述

② 派生类覆盖基类虚函数

  • 再来看一下派生类覆盖了基类的虚函数的情形,可见:
    • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置 (显然的,不然虚函数失去意义);
    • 派生类没有覆盖的虚函数延用基类的。
  • 测试代码:
#include <iostream>

using namespace std;

class Base {
public:
	virtual void f() { cout << "f()" << endl; }
	virtual void g() { cout << "g()" << endl; }
	virtual void h() { cout << "h()" << endl; }
};

class Derive :public Base{
public:
	virtual void x() { cout << "x()" << endl; }
	virtual void f() { cout << "Derive::f()" << endl; }
};

int main() {
	Derive t;
	(((void(*)())   *((int*)(*((int*)&t)))))   ();
	(((void(*)())*((int*)(*((int*)&t)) + 1)))  ();
	(((void(*)())*((int*)(*((int*)&t)) + 2)))  ();
	// (((void(*)())*((int*)(*((int*)&t)) + 3)))  ();

	return 0;
}
  • 测试效果:

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

三、多继承下的虚函数表

① 无虚函数覆盖

  • 如果是多重继承的话,问题就会复杂一些:
    • 每个基类都有自己的虚函数表;
    • 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后(这点和单继承无虚函数覆盖相同)。
  • 如下所示:

在这里插入图片描述

  • 测试代码:
#include <iostream>
class Base {
public:
	Base(int mem1 = 1, int mem2 = 2) : m_iMem1(mem1), m_iMem2(mem2) { ; }

	virtual void vfunc1() { std::cout << "In vfunc1()" << std::endl; }
	virtual void vfunc2() { std::cout << "In vfunc2()" << std::endl; }
	virtual void vfunc3() { std::cout << "In vfunc3()" << std::endl; }

private:
	int m_iMem1;
	int m_iMem2;
};

class Base2 {
public:
	Base2(int mem = 3) : m_iBase2Mem(mem) { ; }
	virtual void vBase2func1() { std::cout << "In Base2 vfunc1()" << std::endl; }
	virtual void vBase2func2() { std::cout << "In Base2 vfunc2()" << std::endl; }

private:
	int m_iBase2Mem;
};

class Base3 {
public:
	Base3(int mem = 4) : m_iBase3Mem(mem) { ; }
	virtual void vBase3func1() { std::cout << "In Base3 vfunc1()" << std::endl; }
	virtual void vBase3func2() { std::cout << "In Base3 vfunc2()" << std::endl; }

private:
	int m_iBase3Mem;
};

class Devired : public Base, public Base2, public Base3 {
public:
	Devired(int mem = 7) : m_iMem1(mem) { ; }
	virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }

private:
	int m_iMem1;
};

int main() {
	// Test_3
	Devired d;
	int *dAddress = (int*)&d;
	typedef void(*FUNC)();

	/* 获取对象的内存布局信息 */
	// 虚表地址一
	int *vtptr1 = (int*)*(dAddress + 0);
	int basemem1 = (int)*(dAddress + 1);
	int basemem2 = (int)*(dAddress + 2);

	int *vtpttr2 = (int*)*(dAddress + 3);
	int base2mem = (int)*(dAddress + 4);

	int *vtptr3 = (int*)*(dAddress + 5);
	int base3mem = (int)*(dAddress + 6);

	/* 输出对象的内存布局信息 */
	int *pBaseFunc1 = (int *)*(vtptr1 + 0);
	int *pBaseFunc2 = (int *)*(vtptr1 + 1);
	int *pBaseFunc3 = (int *)*(vtptr1 + 2);
	int *pBaseFunc4 = (int *)*(vtptr1 + 3);

	(FUNC(pBaseFunc1))();
	(FUNC(pBaseFunc2))();
	(FUNC(pBaseFunc3))();
	(FUNC(pBaseFunc4))();
	// .... 后面省略若干输出内容,可自行补充
	return 0;
}
  • 测试效果:

在这里插入图片描述

② 派生类覆盖基类虚函数

  • 再来看一下派生类覆盖了基类的虚函数的情形,可见:
    • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置;
    • 派生类没有覆盖的虚函数延用基类的。
  • 如下所示,注意这里只给出了类的定义,main 函数的测试代码与上节一样:
class Devired : public Base, public Base2, public Base3 {
public:
	Devired(int mem = 7) : m_iMem1(mem) { ; }
	virtual void vdfunc1() { std::cout << "In Devired vdfunc1()" << std::endl; }
	virtual void vfunc1() { std::cout << "In Devired vfunc1()" << std::endl; }
	virtual void vBase2func1() { std::cout << "In Devired vfunc1()" << std::endl; }

private:
	int m_iMem1;
};
  • 测试效果:

在这里插入图片描述

四、钻石型虚继承

  • 该继承还是遵循上述的所有原则,直接来测试。测试代码如下:
// 虚基指针所指向的虚基表的内容:
// 1. 虚基指针的第一条内容表示的是该虚基指针距离所在的子对象的首地址的偏移
// 2. 虚基指针的第二条内容表示的是该虚基指针距离虚基类子对象的首地址的偏移

#pragma vtordisp(off)
#include <iostream>
using std::cout;
using std::endl;

class B {
public:
	B() : _ib(10), _cb('B') {}

	virtual void f() {
		cout << "B::f()" << endl;
	}

	virtual void Bf() {
		cout << "B::Bf()" << endl;
	}

private:
	int _ib;
	char _cb;
};

class B1 : virtual public B {
public:
	B1() : _ib1(100), _cb1('1') {}

	virtual void f() {
		cout << "B1::f()" << endl;
	}

#if 1
	virtual void f1() {
		cout << "B1::f1()" << endl;
	}
	virtual void Bf1() {
		cout << "B1::Bf1()" << endl;
	}
#endif

private:
	int _ib1;
	char _cb1;
};

class B2 : virtual public B {
public:
	B2() : _ib2(1000), _cb2('2') {}

	virtual void f() {
		cout << "B2::f()" << endl;
	}
#if 1
	virtual void f2() {
		cout << "B2::f2()" << endl;
	}
	virtual void Bf2() {
		cout << "B2::Bf2()" << endl;
	}
#endif
private:
	int _ib2;
	char _cb2;
};

class D : public B1, public B2 {
public:
	D() : _id(10000), _cd('3') {}

	virtual void f() {
		cout << "D::f()" << endl;
	}

#if 1
	virtual void f1() {
		cout << "D::f1()" << endl;
	}
	virtual void f2() {
		cout << "D::f2()" << endl;
	}

	virtual void Df() {
		cout << "D::Df()" << endl;
	}
#endif
private:
	int _id;
	char _cd;
};

int main(void) {
	D d;
	cout << sizeof(d) << endl;
	return 0;
}
  • 测试效果:
1>class D	size(52):
1>	+---
1> 0	| +--- (base class B1)
1> 0	| | {vfptr}
1> 4	| | {vbptr}
1> 8	| | _ib1
1>12	| | _cb1
1>  	| | <alignment member> (size=3)
1>	| +---
1>16	| +--- (base class B2)
1>16	| | {vfptr}
1>20	| | {vbptr}
1>24	| | _ib2
1>28	| | _cb2
1>  	| | <alignment member> (size=3)
1>	| +---
1>32	| _id
1>36	| _cd
1>  	| <alignment member> (size=3)
1>	+---
1>	+--- (virtual base B)
1>40	| {vfptr}
1>44	| _ib
1>48	| _cb
1>  	| <alignment member> (size=3)
1>	+---
1>
1>D::$vftable@B1@:
1>	| &D_meta
1>	|  0
1> 0	| &D::f1
1> 1	| &B1::Bf1
1> 2	| &D::Df
1>
1>D::$vftable@B2@:
1>	| -16
1> 0	| &D::f2
1> 1	| &B2::Bf2
1>
1>D::$vbtable@B1@:
1> 0	| -4
1> 1	| 36 (Dd(B1+4)B)
1>
1>D::$vbtable@B2@:
1> 0	| -4
1> 1	| 20 (Dd(B2+4)B)
1>
1>D::$vftable@B@:
1>	| -40
1> 0	| &D::f
1> 1	| &B::Bf
1>

五、总结

  • 单继承:
    • 虚表中派生类覆盖的虚函数的地址被放在了基类相应的函数原来的位置;
    • 派生类没有覆盖的虚函数就延用基类的,同时虚函数按照其声明顺序放于表中,父类的虚函数在子类的虚函数前面。
  • 多继承:
    • 每个基类都有自己的虚函数表;
    • 派生类的虚函数地址存依照声明顺序放在第一个基类的虚表最后。
  • 安全性问题:当直接通过父类指针调用子类中的未覆盖父类的成员函数,编译器会报错,但通过实验,可以用对象的地址访问到各个子类的成员函数,就违背 C++ 语义,操作会有一定的隐患,使用时要注意。

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

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

相关文章

使用自己修改的特制舵机,支持关节角度回传的桌面级小机器工具人

有人说:一个人从1岁活到80岁很平凡,但如果从80岁倒着活,那么一半以上的人都可能不凡。 生活没有捷径,我们踩过的坑都成为了生活的经验,这些经验越早知道,你要走的弯路就会越少。

c++小知识

1、一般头文件&#xff1a; #include<iostream> using namespace std; 2、ios::sync_with_stdio(false); 其含义是取消cin与stdin的同步&#xff0c;cout和stdout的同步 cin.tie(0); tie是将两个stream绑定的函数&#xff0c;空参数的话返回当前的输出流指针。 3、…

从零开始搭建性能完备的网站-思路过程(1)

之前前端的一揽子技术基本都学完了&#xff0c;除了react和uniapp这些想做大前端需要学的东西&#xff0c;基本vue相关的东西都学过了&#xff0c;本来想做尚硅谷的后台项目&#xff0c;可是它使用的工具库实在是太老了&#xff0c;所以直接跳到自己搭建网站这一步来&#xff0…

尚硅谷大数据技术Zookeeper教程-笔记04【源码解析-源码详解】

视频地址&#xff1a;【尚硅谷】大数据技术之Zookeeper 3.5.7版本教程_哔哩哔哩_bilibili 尚硅谷大数据技术Zookeeper教程-笔记01【Zookeeper(入门、本地安装、集群操作)】尚硅谷大数据技术Zookeeper教程-笔记02【服务器动态上下线监听案例、ZooKeeper分布式锁案例、企业面试真…

C语言标准CRC-32校验函数

C语言标准CRC-32校验函数 CRC-32校验产生4个字节长度的数据校验码&#xff0c;通过计算得到的校验码和获得的校验码比较&#xff0c;用于验证获得的数据的正确性。获得的校验码是随数据绑定获得。 CRC校验原理及标准CRC-8校验函数可参考&#xff1a;C语言标准CRC-8校验函数。…

HTML5 <ol> 标签、HTML5 <object> 标签

HTML5 <ol> 标签 实例 HTML5 <ol>标签用于定义文档中的有序列表。请参考下述示例&#xff1a; 2 个不同的有序列表实例&#xff1a; <ol><li>Coffee</li><li>Tea</li><li>Milk</li> </ol><ol start"50…

低代码平台是否会取代程序员?答案在这里

上图是一张机器人或者自动化系统取代人工的图片&#xff0c;您看了有哪些感想呢&#xff1f; 故事 程序员小张&#xff1a; 刚毕业&#xff0c;参加工作1年左右&#xff0c;日常工作是CRUD 架构师老李&#xff1a; 多个大型项目经验&#xff0c;精通各种屠龙宝术&#xff1b; …

Java 在线编程编译工具上线,直接运行Java代码

前言 大家好&#xff0c;我是小哈~ 周末没出去浪&#xff0c;花了点时间&#xff0c;在我的个人网站上线了一款小工具。啥工具呢&#xff1f;一款可以在线编译 Java 代码并运行输出结果的小工具。 大家都知道&#xff0c;甲骨文刷 Java 版本号非常积极&#xff0c;这不上个月…

【大唐杯学习超快速入门】5G技术原理仿真教学——通信网络认知

这里写目录标题智能通信业务对讲机固定电话电视机wifiPAD扫地机器人手机电信业务号码办理基础业务办理业务选择通信流程模拟增值业务办理工程实践信号塔基站机房传输&核心机房智能通信业务 按照顺序来进行&#xff0c;对讲机&#xff0c;固定电话&#xff0c;电视机&#…

HTTP协议 | 一文详解HTTP报文结构

目录 &#x1f333; HTTP/HTTPS简介 &#x1f333; HTTP工作原理 HTTP三点注意事项 1. HTTP是无连接的 2. HTTP是媒体独立的 3. HTTP是无状态的 HTTPS 作用 &#x1f333; HTTP消息结构 HTTP请求消息 1. 请求行 2. 请求头 3. 空行 4. 请求数据 HTTP请求实例 HTT…

中电金信:数字化转型|银行业数据中心数字化转型之驱动篇

导语&#xff1a; 在新基建和国产化创新的推动下&#xff0c;银行业掀起了数字化转型的浪潮。但在众多银行中&#xff0c;数字化转型仍旧停留在业务层面&#xff0c;数据中心的数字化转型一直处于配合和被动应对。数据中心层面的数字化转型如何开展&#xff0c;数据中心数字化…

springboot的rest服务配置服务的根路径

如果不配置默认为空&#xff0c;如下是application.yml文件只配置了端口号 server:port: 6868 那么访问时直接访问服务即可 如果配置了rest服务 RestController RequestMapping("/netLicense") public class NetLicenseController {RequestMapping("/getLice…

BP神经网络和RBF神经网络的区别

本站原创文章&#xff0c;转载请说明来自《老饼讲解-BP神经网络》 bp.bbbdata.com 有些同学只学过BP神经网络&#xff0c;想了解RBF神经网络 或者只学过RBF神经网络&#xff0c;想了解BP神经网络 那么本文就非常适合这些同学阅读&#xff0c;帮助大家快速将相关知识迁移到BP/RB…

Java——二叉树的镜像

题目链接 leetcode在线oj题——二叉树的镜像 题目描述 请完成一个函数&#xff0c;输入一个二叉树&#xff0c;该函数输出它的镜像。 例如输入&#xff1a; 4 / 2 7 / \ / 1 3 6 9 镜像输出&#xff1a; 4 / 7 2 / \ / 9 6 3 1 题目示例 输入&#xff1a;root [4,2…

sql的左连接(LEFT JOIN)、右连接(RIGHT JOIN)、内连接(INNER JOIN)的详解

sql的左连接&#xff08;LEFT JOIN&#xff09;、右连接&#xff08;RIGHT JOIN&#xff09;、内连接&#xff08;INNER JOIN&#xff09;的详解&#xff1a; 这里以两个表的连表为例&#xff1a; 创建表1&#xff1a;为人员表&#xff0c;这里将它当做左表&#xff1b; CREA…

如何配置达梦数据库使其支持GB18030-2022中文编码字符集

达梦版本要求&#xff0c;要求使用2023年4月及之后的达梦月度版版本&#xff0c;补丁版、临时版等不可以。正确安装windows操作系统&#xff0c;windows10以下的版本未做过测试&#xff0c;可能系统不支持导致各种显示错误&#xff0c;建议用windows10及以上版本&#xff0c;本…

大数据能力提升项目|学生成果展系列之六

导读为了发挥清华大学多学科优势&#xff0c;搭建跨学科交叉融合平台&#xff0c;创新跨学科交叉培养模式&#xff0c;培养具有大数据思维和应用创新的“π”型人才&#xff0c;由清华大学研究生院、清华大学大数据研究中心及相关院系共同设计组织的“清华大学大数据能力提升项…

分子生物学 第一章 概论

文章目录第一章 概论1.1.1分子生物学的概念以及发展简史1.2.1分子生物学研究概况第一章 概论 1.1.1分子生物学的概念以及发展简史 广义的定义&#xff1a; 在分子水平上解释生物学现象。 &#xff08;难以与生物化学区分&#xff09; 严格的定义&#xff1a; 在分子水平上研究…

2023年就业卷,卷,卷!前端面试怎么准备?

本文首发自「慕课网」&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注"慕课网"&#xff01; 作者&#xff1a;张轩|慕课网讲师 大多数开发者应该都经历过跳槽和面试&#xff0c;这也是我们工作生活中必须要经历的一部分&#xff0…

关于Python爬虫使用技巧

首先&#xff0c;Python是一种非常流行的编程语言&#xff0c;拥有广泛的应用领域&#xff0c;例如数据分析、人工智能、Web开发等。如果您是初学者&#xff0c;可以开始学习基础的语法和概念&#xff0c;例如变量、数据类型、循环、函数等等。许多在线资源可以提供学习资料。 …