C++ 虚函数详解(虚函数表、vfptr)——带虚函数表的内存分布图

前言

总所周知,虚函数是实现多态的基础。

  • 引用或指针的静态类型与对象本身的动态类型的不同,才是C++支持多态的根本所在。
  • 当使用基类的引用或指针调用一个虚函数成员时,会执行动态绑定。
  • 所有的虚函数都必须有定义,因为编译器直到运行前也不知道到底要调用哪个版本的虚函数。
  • 只有通过指针或引用调用虚函数才会发生动态绑定,因为只有这种情况,引用或指针的静态类型与对象本身的动态类型才会不同。

关于另一篇博客

大家在网上搜索关于虚函数的博客应该都会搜到陈皓写的那篇C++ 虚函数表解析吧,这篇文章确实不错,画的图也比较好理解,对于指针理解比较深刻的人应该不会理解错误,但对于新人来说可能还是有点不友好。以下几点我觉得需要强调:

  • 虚函数表的指针,实质是指针的指针。
  • 虚函数表的内容,实质是一个指针的数组。(同时辅证了上一点)
  • 在图例中,所以就会两次指针指向的过程。

还有一点就是在该大神的例子程序的输出中,给出的中文解释我认为是错误的,看起来是很容易误导人的。 最开始的例子程序中的:
cout << "虚函数表地址:" << (int*)(&b) << endl;
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
这两句明显错误,本人在困惑之余便开始了自己的验证。
而且图例中也应该有两次指针指向的过程。

虚函数表(vfptr)

虚函数表的指针存储在对象实例中最前面的位置。
这意味着我们可以通过对象实例的地址得到这个虚函数表的指针,然后就遍历虚函数表中的各个函数指针,然后调用相应的函数。
下面开始各个例子程序的实验!(win10+vs2017)

只有基类

#include "pch.h"
#include <iostream>
using namespace std;
class Base {

public:

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

	virtual void g() { cout << "Base::g" << endl; }

	virtual void h() { cout << "Base::h" << endl; }

};
int main()
{
	typedef void(*Fun)(void);

	Base b;

	Fun pFun = NULL;

	Base * p = &b;

	cout << "该对象的地址:" << p << endl;

	cout << "虚函数表的指针也是从这个地址"<< (int*)(&b) <<"开始存的" << endl << endl;

	cout << "虚函数表的指针指向的地址10进制:" << *(int*)(&b) << "即虚函数表的指针存的内容"<<endl;

	cout << "即虚函数表的地址:" << (int*)*(int*)(&b) << endl << endl;

	pFun = (Fun)*(int*)*(int*)(&b);//第一个虚函数的指针

	cout << "第一个虚函数的地址:" << pFun << endl;

	pFun();

	Fun gFun = NULL;
	gFun = (Fun)*((int*)*(int*)(&b) + 1);//第二个虚函数的指针

	Fun hFun = NULL;
	hFun = (Fun)*((int*)*(int*)(&b) + 2);//第三个虚函数的指针
}
  1. 理解内存里每个字节是有编号,这个编号便是我们说的地址。
  2. 指针存的是一个地址,我们只关心指针指向的地址(指针存的地址)和指向对象的类型,而不关心指针这个对象本身的地址。
  3. 对指针解引用,实际是从指针指向的地址的那个字节开始,按照指向对象的类型的字节大小n,读取n个字节出来,来组成这个类型的对象。
  4. 打印指针时,会打印出来指针指向的地址,以16进制。

在这里插入图片描述

  1. b返回Base类型的对象。
  2. &b返回Base *类型的指针。
  3. (int *)(&b)Base *类型的指针转换为int *类型的指针,转换后指针指向地址没变,但指向对象的类型变了。
  4. *(int *)(&b)int *类型的指针解引用,从地址开始的那个字节开始,取出sizeof(int)个字节,赋值给一个int对象(因为指针认为自己指向一个int对象)。
  5. (int *)*(int *)(&b)相当于 (int *)后接一个int值,返回一个int指针,将这个int值作为该指针指向的地址值。
  6. *(int *)*(int *)(&b)int *类型的指针解引用,返回int值。
  7. (Fun)*(int *)*(int *)(&b),Fun是函数指针,后接一个int值,将这个int值作为该函数指针指向的地址值。
  8. 如果以上过程你都正确理解,那么你就能理解这句gFun = (Fun)*((int*)*(int*)(&b) + 1);了。首先(int*)*(int*)(&b)将虚函数表的指针转换为指向指针数组首元素的指针(即转换过程中,指针指向地址没变的),然后((int*)*(int*)(&b) + 1)这里就是数组的指针的正常操作,现在这个指针指向了数组的第二个元素(即第二个虚函数指针),最后就是解引用,然后转换为Fun函数指针。

如果你还没有理解某个步骤,建议直接查看以下图例的大图,配合debug显示的局部变量表使用,再回头看整个过程。
在这里插入图片描述
在这里插入图片描述
上图解释了虚函数的实现机制:

  1. 在有虚函数的基类对象中,肯定至少有三块不同的内存存储区域。
  2. 首先是对象内存空间,其开始区域,存了虚函数表的指针。
  3. 虚函数表实际是一个指针的数组,这些指针就是虚函数的函数指针。
  4. 最后是各个虚函数的存储区域。

虚函数表的结束标志

在上面例子中还需要讲一个细节,在虚函数表最后位置有一个字节用来标志虚函数表的结束。

在这里插入图片描述

	char* end = NULL;
	end = (char*)((int*)*(int*)(&b) + 3);

加入如上代码便可以得到结束标志,((int*)*(int*)(&b) + 3)这里指向了虚函数表即指针数组的第四个元素,但实际上数组里只有三个指针,所以这里便刚好指向了结束标志。再通过(char*)转换指针类型,代表指向的是一个字节。
在这里插入图片描述
由于我是第二次运行程序,所以地址有点不一样。这里end指针存的地址,按照之前的例子应该是0x00305b38再加12。
这里你最好再明确下char型存储的含义:(即ASCII表中:是int型<---->char型的相互转换关系)

	char end1 = '\0';//字符串的结束符
	char end2 = 0;//字符串的结束符
	char zero1 = '0';//这才是真正的字符0
	char zero2 = 48;

在这里插入图片描述

单继承(无虚函数覆盖)

在此例中,基类有三个虚函数,派生类也有三个虚函数,但派生类一个虚函数也没有去重写。
在这里插入图片描述

#include "pch.h"
#include <iostream>
using namespace std;
class Base {
public:

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

	virtual void g() { cout << "Base::g" << endl; }

	virtual void h() { cout << "Base::h" << endl; }

};
class Derive : public Base {
public:

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

	virtual void g1() { cout << "Derive::g" << endl; }

	virtual void h1() { cout << "Derive::h" << endl; }

};
int main()
{
	typedef void(*Fun)(void);
	Derive d;
	Base *p = &d;

	Fun fFun = NULL;
	fFun = (Fun)*((int*)*(int*)(&d) + 0);//第一个虚函数的指针

	Fun gFun = NULL;
	gFun = (Fun)*((int*)*(int*)(&d) + 1);//第二个虚函数的指针

	Fun hFun = NULL;
	hFun = (Fun)*((int*)*(int*)(&d) + 2);//第三个虚函数的指针

	Fun f1 = NULL;
	f1 = (Fun)*((int*)*(int*)(&d) + 3);
	Fun g1 = NULL;
	g1 = (Fun)*((int*)*(int*)(&d) + 4);
	Fun h1 = NULL;
	h1 = (Fun)*((int*)*(int*)(&d) + 5);

	char* end = NULL;
	end = (char*)((int*)*(int*)(&d) + 6);
}

在这里插入图片描述
虽然虚函数表里只能显示父类的虚函数,但通过增加数组指针的方法,一样可以获得派生类的虚函数指针。就算这里是Derive *p1 = &d;也一样,只显示基类的三个虚函数。

  • 虚函数指针按照声明顺序放在虚函数表里面。
  • 基类的虚函数在派生类的虚函数前面。

虚函数表的内存模型如下:
在这里插入图片描述
但这里我已经厌倦了给每个虚函数生成一个函数指针,所以可以用以下循环:

int main()
{
	typedef void(*Fun)(void);
	Derive d;

	int *vTable = (int *)*(int *)(&d);//虚函数表的指针
	for (int i = 0; i<6; ++i)//判断条件写成vTable[i] != 0,有可能会报异常
	{
		printf("function : %d :0X%x->", i, vTable[i]);
		Fun f = (Fun)(vTable[i]);
		f();         //访问虚函数
	}
}

在这里插入图片描述
vTable[i]相当于给vTable指针加i,再解引用。其实就是数组的用法啦,所以就少了解引用的一步。
打印出来的是各个虚函数的地址。

单继承(有虚函数覆盖)

在此例中,派生类只覆盖了基类的一个函数:f()。
在这里插入图片描述

#include "pch.h"
#include <iostream>
using namespace std;
class Base {
public:

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

	virtual void g() { cout << "Base::g" << endl; }

	virtual void h() { cout << "Base::h" << endl; }

};
class Derive : public Base {
public:

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

	virtual void g1() { cout << "Derive::g" << endl; }

	virtual void h1() { cout << "Derive::h" << endl; }

};
int main()
{
	typedef void(*Fun)(void);
	Derive d;

	int *vTable = (int *)*(int *)(&d);
	for (int i = 0; i<5; ++i)
	{
		printf("function : %d :0X%x->", i, vTable[i]);
		Fun f = (Fun)(vTable[i]);
		f();         //访问虚函数
	}
}

在这里插入图片描述在这里插入图片描述
可以看出:

  • 由于f虚函数被重写,原本虚函数表(即指针数组)第一个元素是Base::f()的指针,现在被替换为了Derive::f()的指针
  • 其他虚函数按照之前的顺序排列

虚函数表的内存模型如下:
在这里插入图片描述

多重继承(无虚函数覆盖)

在此例中,有三个基类,一个派生类,且派生类一个虚函数也没有去重写。
在这里插入图片描述

#include "pch.h"
#include <iostream>
using namespace std;
class Base1 {
public:
	virtual void f() { cout << "Base1::f" << endl; }
	virtual void g() { cout << "Base1::g" << endl; }
	virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
	virtual void f() { cout << "Base2::f" << endl; }
	virtual void g() { cout << "Base2::g" << endl; }
	virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
	virtual void f() { cout << "Base3::f" << endl; }
	virtual void g() { cout << "Base3::g" << endl; }
	virtual void h() { cout << "Base3::h" << endl; }
};
class Derive : public Base1,public Base2, public Base3 {
public:
	virtual void f1() { cout << "Derive::f" << endl; }
	virtual void g1() { cout << "Derive::g" << endl; }
	virtual void h1() { cout << "Derive::h" << endl; }
};
typedef void(*Fun)(void);
void printVfun(int n,int * vTable) {
	for (int i = 0; i < n; ++i)
	{
		printf("function : %d :0X%x->", i, vTable[i]);
		Fun f = (Fun)(vTable[i]);
		f();         //访问虚函数
	}
	cout << "" << endl;
}
int main()
{
	Derive d;

	int *vTable1 = (int *)*(int *)(&d);//第一个虚函数表的指针
	printVfun(6, vTable1);

	int *vTable2 = (int *)*((int *)(&d)+1);//第二个虚函数表的指针
	printVfun(3, vTable2);

	int *vTable3 = (int *)*((int *)(&d) + 2);//第三个虚函数表的指针
	printVfun(3, vTable3);
}

在这里插入图片描述
在这里插入图片描述
可以看到:

  • 对于继承到的每个基类,都有一个对应的虚函数表。
  • 派生类的虚函数的指针,被放进了第一个基类对应的虚函数表里。(按照声明顺序来判断的)

内存模型如下:
在这里插入图片描述

多重继承(有虚函数覆盖)

在此例中,有三个基类,一个派生类,且派生类重写了三个基类的同一个虚函数。
在这里插入图片描述

#include "pch.h"
#include <iostream>
using namespace std;
class Base1 {
public:
	virtual void f() { cout << "Base1::f" << endl; }
	virtual void g() { cout << "Base1::g" << endl; }
	virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
	virtual void f() { cout << "Base2::f" << endl; }
	virtual void g() { cout << "Base2::g" << endl; }
	virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
	virtual void f() { cout << "Base3::f" << endl; }
	virtual void g() { cout << "Base3::g" << endl; }
	virtual void h() { cout << "Base3::h" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
	virtual void f() { cout << "Derive::f" << endl; }
	virtual void g1() { cout << "Derive::g" << endl; }
	virtual void h1() { cout << "Derive::h" << endl; }
};
typedef void(*Fun)(void);
void printVfun(int n, int * vTable) {
	for (int i = 0; i < n; ++i)
	{
		printf("function : %d :0X%x->", i, vTable[i]);
		Fun f = (Fun)(vTable[i]);
		f();         //访问虚函数
	}
	cout << "" << endl;
}
int main()
{
	Derive d;

	int *vTable1 = (int *)*(int *)(&d);//第一个虚函数表的指针
	printVfun(5, vTable1);

	int *vTable2 = (int *)*((int *)(&d) + 1);//第二个虚函数表的指针
	printVfun(3, vTable2);

	int *vTable3 = (int *)*((int *)(&d) + 2);//第三个虚函数表的指针
	printVfun(3, vTable3);
}

在这里插入图片描述
在这里插入图片描述
可以看到:

  • 三个基类的虚函数表的第一项,都被替换为Derive::f的指针
  • 这样任意基类指针指向派生类对象,都可以调用到Derive::f

对象模型如下:
在这里插入图片描述

类与虚函数表与虚函数的对应关系

注意本章中的示意图都只会关注基类的虚函数指针。或者因为重写,而导致在虚函数表中基类的虚函数指针被替换的情况。(就像局部变量图中的一样)

单继承(无虚函数覆盖)

在该例中运行:

	Base b1;
	Base b2;
	Derive d1;
	Derive d2;

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

  • 每一个类对应到一个虚函数表。
  • 两个虚函数表里各个指针指向的地址都是相同的。

单继承(有虚函数覆盖)

	Base b;
	Derive d;

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

  • 基类的虚函数表的三项还是没有变化
  • 派生类的虚函数表的第一项被替换了

多重继承(无虚函数覆盖)

	Base1 b1;
	Base2 b2;
	Base3 b3;
	Derive d;

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

  • 派生类因为继承了三个基类,所以会有三张虚函数表。

多重继承(有虚函数覆盖)

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

  • 派生类的每个虚函数表的第一项都被替换为Derive::f()的指针了,因为它把三个基类的f虚函数都重写了。
  • 10
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值