前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >深入探索虚函数表(详细)

深入探索虚函数表(详细)

原创
作者头像
用户7118204
修改于 2020-04-07 03:53:03
修改于 2020-04-07 03:53:03
55800
代码可运行
举报
运行总次数:0
代码可运行

这篇博客可能有一点点长,代码也有一点点多,但是仔细阅读分析完,会对虚函数表有一个深刻的认识。

什么是虚函数表?

       对于一个类来说,如果类中存在虚函数,那么该类的大小就会多4个字节,然而这4个字节就是一个指针的大小,这个指针指向虚函数表。所以,如果对象存在虚函数,那么编译器就会生成一个指向虚函数表的指针,所有的虚函数都存在于这个表中,虚函数表就可以理解为一个数组,每个单元用来存放虚函数的地址。


 虚函数(Virtual Function)是通过一张虚函数表来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。                                                                                                                                   -------------百度百科

 虚函数表存在的位置

       由于虚函数表是由编译器给我们生成的,那么编译器会把虚函数表安插在哪个位置呢?下面可以简单的写一个示例来证明一下虚函数表的存在,以及观察它所存在的位置,先来看一下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <stdio.h>
using namespace std;
 
class A{
public:
	int x;
	virtual void b() {}
};
 
int main()
{
	A* p = new A;
	cout << p << endl;
	cout << &p->x << endl;
	return 0;
}

       定义了一个类A,含有一个x和一个虚函数,实例化一个对象,然后输出对象的地址和对象成员x的地址,我们想一下,如果对象的地址和x的地址相同,那么就意味着编译器把虚函数表放在了末尾,如果两个地址不同,那么就意味着虚函数表是放在最前面的。运行结果为16进制,然后我们把它转为10进制观察一下:

       可以观察到结果是不同的,而且正好相差了4个字节,由此可见,编译器把生成的虚函数表放在了最前面。

获取虚函数表

       既然虚函数表是真实存在的,那么我们能不能想办法获取到虚函数表呢?其实我们可以通过指针的形式去获得,因为前面也提到了,我们可以把虚函数表看作是一个数组,每一个单元用来存放虚函数的地址,那么当调用的时候可以直接通过指针去调用所需要的函数就行了。我们就类比这个思路,去获取一下虚函数表。首先先定义两个类,一个是基类一个是派生类,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base {
public:
	virtual void a() { cout << "Base a()" << endl; }
	virtual void b() { cout << "Base b()" << endl; }
	virtual void c() { cout << "Base c()" << endl; }
};
 
class Derive : public Base {
public:
	virtual void b() { cout << "Derive b()" << endl; }
};

       现在我们设想一下Derive类中的虚函数表是什么样的,它应该是含有三个指针,分别指向基类的虚函数a和基类的虚函数c和自己的虚函数b(因为基类和派生类中含有同名函数,被覆盖),那么我们就用下面的方式来验证一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    Derive* p = new Derive;
	long* tmp = (long*)p;             // 先将p强制转换为long类型指针tmp
 
	// 由于tmp是虚函数表指针,那么*tmp就是虚函数表
	long* vptr = (long*)(*tmp);
	for (int i = 0; i < 3; i++) {
		printf("vptr[%d] : %p\n", i, vptr[i]);
	}

       同理,我们把基类的虚函数表的内容也用这种方法获取出来,然后二者进行比较一下,看看是否是符合我们上面所说的那个情况。先看一下完整的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <stdio.h>
using namespace std;
 
class Base {
public:
	virtual void a() { cout << "Base a()" << endl; }
	virtual void b() { cout << "Base b()" << endl; }
	virtual void c() { cout << "Base c()" << endl; }
};
 
class Derive : public Base {
public:
	virtual void b() { cout << "Derive b()" << endl; }
};
 
int main()
{
	cout << "-----------Base------------" << endl;
	Base* q = new Base;
	long* tmp1 = (long*)q;
	long* vptr1 = (long*)(*tmp1);
	for (int i = 0; i < 3; i++) {
		printf("vptr[%d] : %p\n", i, vptr1[i]);
	}
 
	Derive* p = new Derive;
	long* tmp = (long*)p;
	long* vptr = (long*)(*tmp);
	cout << "---------Derive------------" << endl;
	for (int i = 0; i < 3; i++) {
		printf("vptr[%d] : %p\n", i, vptr[i]);
	}
	return 0;
}

       运行结果如下图所示:

       可见基类中的三个指针分别指向a,b,c虚函数,而派生类中的三个指针中第一个和第三个和基类中的相同,那么这就印证了上述我们所假设的情况,那么这也就是虚函数表。但是仅仅只是观察指向的地址,还不是让我们观察的特别清楚,那么我们就通过定义函数指针,来调用一下这几个地址,看看结果是什么样的,下面直接上代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <stdio.h>
using namespace std;
 
class Base {
public:
	virtual void a() { cout << "Base a()" << endl; }
	virtual void b() { cout << "Base b()" << endl; }
	virtual void c() { cout << "Base c()" << endl; }
};
 
class Derive : public Base {
public:
	virtual void b() { cout << "Derive b()" << endl; }
};
 
int main()
{
	typedef void (*Func)();
	cout << "-----------Base------------" << endl;
	Base* q = new Base;
	long* tmp1 = (long*)q;
	long* vptr1 = (long*)(*tmp1);
	for (int i = 0; i < 3; i++) {
		printf("vptr[%d] : %p\n", i, vptr1[i]);
	}
	Func a = (Func)vptr1[0];
	Func b = (Func)vptr1[1];
	Func c = (Func)vptr1[2];
	a();
	b();
	c();
 
	Derive* p = new Derive;
	long* tmp = (long*)p;
	long* vptr = (long*)(*tmp);
	cout << "---------Derive------------" << endl;
	for (int i = 0; i < 3; i++) {
		printf("vptr[%d] : %p\n", i, vptr[i]);
	}
	Func d = (Func)vptr[0];
	Func e = (Func)vptr[1];
	Func f = (Func)vptr[2];
	d();
	e();
	f();
 
	
	return 0;
}

       运行结果如下:

       这样就清晰的印证了上述所说的假设,那么虚函数表就获取出来了。

多重继承的虚函数表:

       虚函数的引入其实就是为了实现多态,现在来研究一下多重继承的虚函数表是什么样的,首先我们先来看一下简单的一般继承的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base1 {
public:
	virtual void A() { cout << "Base1 A()" << endl; }
	virtual void B() { cout << "Base1 B()" << endl; }
	virtual void C() { cout << "Base1 C()" << endl; }
};
 
class Derive : public Base1{
public:
	virtual void MyA() { cout << "Derive MyA()" << endl; }
};

       这是一个类继承一个类,这段代码如果我们通过派生类去调用基类的函数,应该结果可想而知,这里就不再演示和赘述了。我们来分析这两个类的虚函数表,对于基类的虚函数表其实和上面所说的虚函数表是一样的,有自己的虚函数指针,并指向自己的虚函数表,重点是在于派生类的虚函数表是什么样子的,它的样子如下图所示:

       那么Derive的虚函数表就是继承了Base1的虚函数表,然后自己的虚函数放在后面,因此这个虚函数表的顺序就是基类的虚函数表中的虚函数的顺序+自己的虚函数的顺序。那么我们现在在Derive中再添加一个虚函数,让它覆盖基类中的虚函数,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base1 {
public:
	virtual void A() { cout << "Base1 A()" << endl; }
	virtual void B() { cout << "Base1 B()" << endl; }
	virtual void C() { cout << "Base1 C()" << endl; }
};
 
class Derive : public Base1{
public:
	virtual void MyA() { cout << "Derive MyA()" << endl; }
	virtual void B() { cout << "Derive B()" << endl; }
};

       那么对于这种情况的虚函数表如下图所示:

     这个是单继承的情况,然后我们看看多重继承,也就是Derive类继承两个基类,先看一下代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
class Base1 {
public:
	virtual void A() { cout << "Base1 A()" << endl; }
	virtual void B() { cout << "Base1 B()" << endl; }
	virtual void C() { cout << "Base1 C()" << endl; }
};
 
class Base2 {
public:
	virtual void D() { cout << "Base2 D()" << endl; }
	virtual void E() { cout << "Base2 E()" << endl; }
};
 
class Derive : public Base1, public Base2{
public:
	virtual void A() { cout << "Derive A()" << endl; }           // 覆盖Base1::A()
	virtual void D() { cout << "Derive D()" << endl; }           // 覆盖Base2::D()
	virtual void MyA() { cout << "Derive MyA()" << endl; }
};

       首先我们明确一个概念,对于多重继承的派生类来说,它含有多个虚函数指针,对于上述代码而言,Derive含有两个虚函数指针,所以它不是只有一个虚函数表,然后把所有的虚函数都塞到这一个表中,为了印证这一点,我们下面会印证这一点,首先我们先来看看这个多重继承的图示:

       由图可以看出,在第一个虚函数表中首先继承了Base1的虚函数表,然后将自己的虚函数放在后面,对于第二个虚函数表中,继承了Base2的虚函数表,由于在Derive类中有一个虚函数D覆盖了Base2的虚函数,所以第一个表中就没有Derive::D的函数地址。那么我们就用代码来实际的验证一下是否会存在两个虚函数指针,以及如果存在两个虚函数表,那么虚函数表是不是这个样子的。来看下面的代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
#include <stdio.h>
using namespace std;
 
class Base1 {
public:
	virtual void A() { cout << "Base1 A()" << endl; }
	virtual void B() { cout << "Base1 B()" << endl; }
	virtual void C() { cout << "Base1 C()" << endl; }
};
 
class Base2 {
public:
	virtual void D() { cout << "Base2 D()" << endl; }
	virtual void E() { cout << "Base2 E()" << endl; }
};
 
class Derive : public Base1, public Base2{
public:
	virtual void A() { cout << "Derive A()" << endl; }           // 覆盖Base1::A()
	virtual void D() { cout << "Derive D()" << endl; }           // 覆盖Base2::D()
	virtual void MyA() { cout << "Derive MyA()" << endl; }
};
 
int main()
{
	typedef void (*Func)();
	Derive d;
	Base1 &b1 = d;
	Base2 &b2 = d;
	cout << "Derive对象所占的内存大小为:" << sizeof(d) << endl;
	
	cout << "\n---------第一个虚函数表-------------" << endl;
	long* tmp1 = (long *)&d;              // 获取第一个虚函数表的指针
	long* vptr1 = (long*)(*tmp1);         // 获取虚函数表
 
	Func x1 = (Func)vptr1[0];
	Func x2 = (Func)vptr1[1];
	Func x3 = (Func)vptr1[2];
	Func x4 = (Func)vptr1[3];
	x1();x2();x3();x4();
 
	cout << "\n---------第二个虚函数表-------------" << endl;
	long* tmp2 = tmp1 + 1;               // 获取第二个虚函数表指针 相当于跳过4个字节
	long* vptr2 = (long*)(*tmp2);
 
	Func y1 = (Func)vptr2[0];
	Func y2 = (Func)vptr2[1];
	y1(); y2();
 
	return 0;
}

       先看看运行结果,然后再去分析证明:

       因为在包含一个虚函数表的时候,含有一个虚函数表指针,所占用的大小为4个字节,那么这里输出了8个字节,就说明Derive对象含有两个虚函数表指针。然后我们通过获取到了这两个虚函数表,并调用其对应的虚函数,可以发现输出的结果和上面的示例图是相同的,因此就证明了上述所说的结论是正确的。

简单的总结一下:

1. 每一个基类都会有自己的虚函数表,派生类的虚函数表的数量根据继承的基类的数量来定。 2. 派生类的虚函数表的顺序,和继承时的顺序相同。 3. 派生类自己的虚函数放在第一个虚函数表的后面,顺序也是和定义时顺序相同。 4. 对于派生类如果要覆盖父类中的虚函数,那么会在虚函数表中代替其位置。

虚函数指针和虚函数表的创建时机:

       对于虚函数表来说,在编译的过程中编译器就为含有虚函数的类创建了虚函数表,并且编译器会在构造函数中插入一段代码,这段代码用来给虚函数指针赋值。因此虚函数表是在编译的过程中创建。

       对于虚函数指针来说,由于虚函数指针是基于对象的,所以对象在实例化的时候,虚函数指针就会创建,所以是在运行时创建。由于在实例化对象的时候会调用到构造函数,所以就会执行虚函数指针的赋值代码,从而将虚函数表的地址赋值给虚函数指针。

虚函数表的深入探索:

       经过上面的学习说明,我们知道了虚函数表的作用,是用来存放虚函数的地址的,那么我们先来看一下这个代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
#include <iostream>
using namespace std;
 
class A{
public:
	int x;
	A(){
		memset(this, 0, sizeof(x));       // 将this对象中的成员初始化为0
		cout << "构造函数" << endl;
	}
	A(const A& a) {
		memcpy(this, &a, sizeof(A));      // 直接拷贝内存中的内容
		cout << "拷贝构造函数" << endl;
	}
	virtual void virfunc() {
		cout << "虚函数func" << endl;
	}
	void func() {
		cout << "func函数" << endl;
	}
	virtual ~A() {
		cout << "析构函数" << endl;
	}
};
 
int main()
{
	A a;
	a.virfunc();
	return 0;
}

       在构造函数中用的是memset()函数进行初始化操作,在拷贝构造函数中使用memcpy的方式来拷贝,可能这样的方法效率会更高,其运行结果如下图所示:

       可以运行,但是我们要对代码进行分析,前面我们提到了虚函数表是在编译的时候就已经生成好了,那么对于上面的代码中的virfunc来说,它的地址就已经存在于虚函数表中了,又根据前面我们提到的,在实例化对象的时候,编译器会为构造函数中插入一些代码,这些代码用来给虚函数指针进行赋值,那么这些操作都是在我们执行memset之前进行的,因此在执行了这些操作后,调用了memset函数,使得所有内容都清空了,那么虚函数指针就指向了0,那为什么我们还可以调用virfunc函数和析构函数呢?

       这里就涉及到了静态联编和动态联编的问题,我们先来明确一下静态联编和动态联编的定义:

静态联编:在编译时所进行的这种联编又称静态束定,在编译时就解决了程序中的操作调用与执行该操作代码间的关系。 动态联编:编译程序在编译阶段并不能确切知道将要调用的函数,只有在程序运行时才能确定将要调用的函数,为此要确                      切知道该调用的函数,要求联编工作要在程序运行时进行,这种在程序运行时进行联编工作被称为动态联编。

 由于我们把虚函数表指针设为了0,所以我们就无法通过前面的方法来获取它,这里我们可以通过反汇编来查看virfunc的地址:

          我们发现virfunc函数的地址并不是我们设置的0,那么它就变的和普通的函数没有什么区别了(普通函数采用静态联编,在编译时就绑定了函数的地址),这显然不是我们想要的虚函数,那么肯定就无法实现多态,对于类的多态性,一定是基于虚函数表的,那么虚函数表的实现一定是动态联编的,因此也不可缺少虚函数指针寻址的过程,那么我们要实现动态联编,就需要用到指针或者引用的形式。如果按上面代码的方式去执行,由于是非指针非引用的形式,所以编译器采用了静态绑定,提前绑定了函数的地址,那么就不存在指针寻址的过程了;如果使用指针或引用的形式,那么由于对象的所属类不能确定,那么编译器就无法采用静态编联,所以就只有通过动态编联的方式,在运行时去绑定调用指针和被调用地址。那么我们把代码改成下面的样子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
A *a = new A;
a.virfunc();

       这个时候我们再运行程序,由于我们将虚函数指针置为0,从而找不到了虚函数的位置,那么程序就会崩溃,然后我们再通过反汇编查看一下,如图所示:

       比之前的多了一些汇编指令,其中mov就是寻址的过程,这样就实现了动态绑定,也是根据这一点来实现多态性,这里我只用了一个类来进行展示说明,其实用两个类更好。总之,如果要实现虚函数和多态,就需要通过指针或者引用的方式。

       关于虚函数表的东西就是这么多,如果有错误或者遗漏或者有疑问的地方可以在评论区中指出,这篇博客主要是自己学习后的一个总结,对于讲解部分,感觉图片太少了,单纯用文字描述又过于抽象,以后应该要加以改正。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
腾讯云Java SDK maven使用方式的详细介绍
①比较VS2019和VS Code,Visual Studio 2019支持C#/C++/Python/Node.js,安装VS2019,腾讯云好几种SDK都能使用了,真方便,但是不支持Java,而VS Code轻量支持Java,够用了
Windows技术交流
2020/03/17
6.1K0
Maven相关:mvn的配置和简单命令
3. 生成target目录,编译、测试代码,生成测试报告,生成jar/war文件 :
JQ实验室
2022/02/09
1.1K0
Maven相关:mvn的配置和简单命令
maven常见命令及打包方式
可以打包后的文件存放到项目的 target 目录下,打包好的文件通常都是编译后生成的class文件。
全栈程序员站长
2022/08/25
9690
Maven (mvn) 详解:从入门到精通
Maven 是一款基于 POM(Project Object Model)的构建工具,主要用于项目依赖管理、生命周期管理和自动化构建。无论是小型项目还是企业级应用,Maven 都提供了强大的功能,帮助开发者提高开发效率。
默 语
2024/12/24
4240
使用mvn创建java工程的极简教程
最近开始集中精力搞java的开源项目,在这时记录一下使用mvn创建java工程的比较简单的流程,以备不时之需,适用于我等java小白~~~ 文中相关代码已上传: https://github.com/DavidLiuXh/ExampleBank 使用mvn创建java工程 安装mvn 我们之前参考官网就好: Installing Apache Maven 创建java工程 命令行执行 mvn archetype:generate -DgroupId=com.mytest -DartifactId=tes
扫帚的影子
2020/03/20
7760
入门maven?本文足矣
以前开发的时候,如果A项目需要某个jar包,可能去网上搜索下载jar包,然后复制粘贴在开发对应的位置,如果B项目也需要这个jar包,那么同样需要再次手动复制粘贴到对应的位置。
用户8902830
2022/09/22
6670
入门maven?本文足矣
1. Spring 简介 + Hello World
在 Java 近20年的历史中,它经历过很好的时代,也经历过饱受诟病的时代。尽管有很多粗糙的地方,如 applet、企业级JavaBean(Enterprise JavaBean,EJB)、Java 数据对象(Java Data Object,JDO)以及无数的日志框架,但是作为一个平台,Java的历史是丰富多彩的,有很多的企业级软件都是基于这个平台构建的。Spring是Java历史中很重要的组成部分。
acc8226
2022/05/17
2300
maven搭建详解
1, 下载 官方下载地址: maven_download, 最新版 apache-maven-3.0.5-bin.tar.gz 官方地址: maven 2, 解压 tar zxvf apache-maven-3.0.5-bin.tar.gz (例如安装目录为: /home/homer/Apache-maven/apache-maven-3.0.5) 3, 安装 1) 编辑 /etc/profile sudo vi /etc/profile 2) 配置 配置maven安装目录: export MAVEN_HO
程序员同行者
2018/07/02
9100
Maven NetBeans(下)
Maven 将会开始构建该项目。你可以在 NetBeans 的终端里查看输出的日志信息:
陈不成i
2021/07/15
3620
04 . Jenkins部署Java项目
https://img2020.cnblogs.com/blog/1871335/202006/1871335-20200604173550160-1696672787.png
iginkgo18
2020/09/27
7030
04 . Jenkins部署Java项目
【Maven 官方教程】Building Java Projects
mkdir -p src/main/java/hello on *nix systems
acc8226
2022/05/17
3480
一个小时学会Maven
在开发中经常需要依赖第三方的包,包与包之间存在依赖关系,版本间还有兼容性问题,有时还里要将旧的包升级或降级,当项目复杂到一定程度时包管理变得非常重要。
张果
2018/10/09
2.7K0
一个小时学会Maven
【Java 进阶篇】Maven 使用详解:打造便捷高效的项目构建利器
在软件开发的道路上,项目构建是一个不可避免的过程。而Maven,作为一个强大的项目管理和构建工具,为开发者提供了一套标准化的项目结构和构建流程。本文将围绕Maven的使用详解,手把手地带你探索Maven的世界,让你在项目构建的征途上更加得心应手。
繁依Fanyi
2024/01/02
1.4K0
【Java 进阶篇】Maven 使用详解:打造便捷高效的项目构建利器
IDEA配置Maven教程{收藏}
maven是apache组织开发的一款开源的可以管理JavaWeb项目jar包的构建,可以解决jar包之间的冲突;可以对项目进行管理,比如项目打包等,maven使用pom项目对象模型进行构建项目的。
框架师
2019/09/19
7.6K0
IDEA配置Maven教程{收藏}
Maven入门教程
Maven是Java项目构建工具,可以用于管理Java依赖,还可以用于编译、打包以及发布Java项目,类似于JavaScript生态系统中的NPM。
Fundebug
2019/01/08
1.1K0
安装适用于 Java 的 TensorFlow安装适用于 Java 的 TensorFlow
TensorFlow 可提供在 Java 程序中使用的 API。这些 API 特别适合用于加载以 Python 语言创建的模型并在 Java 应用中运行这些模型。本指南将介绍如何安装适用于 Java 的 TensorFlow 并在 Java 应用中使用 TensorFlow。
一个会写诗的程序员
2018/08/17
1.2K0
Maven入门
首先,你需要创建一个项目用来给Maven构建。把注意力集中到Maven上,项目做得尽可能简单。项目结构如下。
_淡定_
2019/04/04
4740
Maven
之前我们导入依赖的时候,每次都要去下载对应的 Jar 包,这样其实是很麻烦的,并且还有可能一个 Jar 包依赖于另一个 Jar 包,因此我们需要一个更加方便的包管理机制。
小简
2023/01/04
7130
Maven
maven常用命令集合(收藏大全)
如果你是初学者,或者是自学者!你可以加小编微信(xxf960326)!小编可以给你学习上,工作上的一些建议以及可以给你(免费)提供学习资料!最重要我们还可以交个朋友!你在学习上有什么问题都可以加小编微信进行私聊!小编都会为你解答!
Java学习
2018/08/27
11.8K0
idea 配置Maven(哈弗f7x科技版配置)
新写的文章地址链接:https://weixiaodyanlei.xyz/archives/idea-chuang-jian-maven-gong-cheng#SnMnpRGS
全栈程序员站长
2022/07/25
2880
idea 配置Maven(哈弗f7x科技版配置)
相关推荐
腾讯云Java SDK maven使用方式的详细介绍
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档