C++ 虚函数表图解

一、前言

一直以来,对虚函数的理解仅仅是,在父类中定义虚函数,子类中可以重写该虚函数,并且父类指针可以指向子类对象,调用子类的虚函数(多态)。在读研阶段经历的几个项目中,自己所写的类中并没有用到虚函数,对虚函数这个东西的强大之处并没有太多体会。最近,学了设计模式中的简单工厂模式,对多态有了具体的认识。于是,补了补多态、虚函数、虚函数表相关的知识,参考相关博客,加上自己的理解,整理了这篇博文。

二、含有虚函数类的内存模型

以下面的类为例(32位平台下):

class Father {
public:
	virtual void fun1() { cout << "Father::fun1()" << endl; }
	virtual void fun2() { cout << "Father::fun2()" << endl; }
	int i;
};

该类中含有两个虚函数和一个成员变量i,输出sizeof(Father),结果为8个字节。如果去掉virtual关键字,则结果为4个字节。也就是说,类中含有虚函数,则该类会增加4个字节,那这4个字节是什么变量所占据的呢?

答案是一个指针(我觉得应该是unsigned int*类型指针,这点不确定),在vs调试窗口中,可以看到该指针名为_vfptr,该指针称为虚函数表指针。

类的内存模型如下,_vptr指针和成员变量i各占4字节,一共8字节。另外 ,-vptr指针一定在内存模型前面。对于只有一个虚表指针的类来说,类内存模型前4个字节就是虚表指针所占空间。

在这里插入图片描述

三、虚函数表与虚函数表指针

上面提到_vfptr是虚函数表指针,那虚函数表是什么呢?

虚函数表其实就是一个指针数组,这个数组中存放着虚函数的地址,大概如下:
在这里插入图片描述
最后一个类似于字符串的结束标志位,VS编译器中为0。

这样的话,虚函数表指针就很容易理解了,这个虚函数表指针指向该虚函数表,也就是虚函数指针的值就是上述指针数组的首地址。
在这里插入图片描述

四、虚函数地址

函数存放在代码区,虚函数也不例外。虚函数表中存放的是虚函数地址,即代码区虚函数的入口地址。
在这里插入图片描述

五、多态

定义一个Father的子类Son,对虚函数fun1()进行重写。

#include<iostream>
using namespace std;
class Father {
public:
	virtual void fun1() { cout << "Father::fun1()" << endl; }
	virtual void fun2() { cout << "Father::fun2()" << endl; }
	int i;
};

class Son :public Father {
	virtual void fun1() { cout << "Son::fun1()" << endl; }

};
int main()
{
	Son son;
	Father father;
	Father *p = &father;
	p->fun1();
	p->fun2();
    p=&son;
    p->fun1();
    p->fun2();
	return 0;
}

父类中有虚函数,则子类同样会有一个虚函数指针,这个指针指向一个新表,如下图所示:
在这里插入图片描述
Son类重写了fun1(),未重写fun2(),那么虚函数表中,第一个地址便是重写的Son::fun1()的地址,第二个地址仍然是父类中Father::fun2()的地址。这里可以在vs调试模式下,查看father与son的虚函数表,son表中第二个元素值与father表中第二个元素值相同。

	Father *p = &father;
	p->fun1();
	p->fun2();

p指向Father对象father:

p->fun1():沿着框1->框3->框5的路径,调用Father::fun1();

p->fun2():沿着框1->框4->框6的路径,调用Father::fun2();

    p=&son;
    p->fun1();
    p->fun2();

p指向子类对象son:

p->fun1():沿着框7->框9->框11的路径,调用Son::fun1();

p->fun2():沿着框7->框10->框6的路径,调用Father::fun2();

六、通过函数指针操作调用虚函数

现修改main函数

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

	Father father;
	Son son;
	printf("虚函数表地址_vfptr:%p\n", *(unsigned int*)(&father));
	printf("第一个虚函数地址e:%p\n", *(unsigned int*)*(unsigned int*)(&father));
	printf("第二个虚函数地址f:%p\n", *((unsigned int*)*(unsigned int*)(&father)+1));

	unsigned char* end = NULL;
	end = (unsigned char*)((unsigned int*)*(unsigned int*)(&father) + 2);
	printf("结束符地址d:%p\n", end);
	printf("结束符值:%d\n", *end);

	Fun pFun = NULL;
	pFun = (Fun)(*((unsigned int*)*(unsigned int*)(&father) + 1));
	pFun();
    
	return 0;
}

运行结果:
在这里插入图片描述
先看一下vs调试模式下各变量的值
在这里插入图片描述

将之前的图修改一下,便于理解:
在这里插入图片描述

红框中,father中的_vfptr为0x1270234,对应上图中的_vfptr,即虚函数表的地址;

数组[0]值为0x01261447,对应上图中的e,即Father::fun1()的地址;

数组[1]值为0x0126141a,对应上图中的f,即Father::fun2()的地址。

好了,现在来看一下程序中这些看起来很唬人的东西:

	printf("虚函数表地址_vfptr:%p\n", *(unsigned int*)(&father));
	printf("第一个虚函数地址e:%p\n", *(unsigned int*)*(unsigned int*)(&father));
	printf("第二个虚函数地址f:%p\n", *((unsigned int*)*(unsigned int*)(&father)+1));
	unsigned char* end = NULL;
	end = (unsigned char*)((unsigned int*)*(unsigned int*)(&father) + 2);

这里,直接用上图中的符号进行分析,否则,说一通xx的地址、对xx解引用等等,容易把人搞晕。

(1)虚函数表的地址*(unsigned int*)(&father)
  • a0=&father

  • a1=(unsigned int *)a0

  • _vfptr=*a1
    在这里插入图片描述
    注意,这里a0和a1的数值是一样的,但只有把地址a0强制转换成(unsigned int *)类型,解引用时系统才会从该地址向后解析4个字节空间,解析成一个unsinged int类型数据。

(2)第一个虚函数地址*(unsigned int*)*(unsigned int*)(&father)
  • b=(unsigned int*)_vfptr

  • e=*b
    在这里插入图片描述

(3)第二个虚函数地址*((unsigned int*)*(unsigned int*)(&father)+1)
  • c=b+1

  • f=*c
    在这里插入图片描述

(4)结束符地址(unsigned char*)((unsigned int*)*(unsigned int*)(&father) + 2)
  • 未强制转换的d=b+2
  • 强制转换的d=(unsigned char*)(b+2)
    在这里插入图片描述
(5)通过函数指针调用虚函数
	Fun pFun = NULL;
	pFun = (Fun)(*((unsigned int*)*(unsigned int*)(&father) + 1));
	pFun();

在这里插入图片描述

七、结语

这篇博文包含许多自己理解的内容,并在此基础上画了图解,如果有误,还请指正。

八、参考

C++ 虚函数表解析

虚函数表详解

C++ 虚函数详解(虚函数表、vfptr)

### 除 `AfxOleInit` 外用于初始化和反初始化的函数 在非 MFC 环境或者更底层的操作中,可使用 `CoInitialize` 和 `CoInitializeEx` 来初始化 COM 库。`CoInitialize` 函数用于初始化 COM 库,为当前线程准备使用 COM 库,其函数原型如下: ```cpp HRESULT CoInitialize( LPVOID pvReserved ); ``` `CoInitializeEx` 是 `CoInitialize` 的扩展版本,允许指定并发模型,如单线程单元(STA)或多线程单元(MTA),其函数原型如下: ```cpp HRESULT CoInitializeEx( LPVOID pvReserved, DWORD dwCoInit ); ``` 对于反初始化,使用 `CoInitialize` 或 `CoInitializeEx` 初始化时,需要调用 `CoUninitialize` 函数来反初始化 COM 库,其函数原型如下: ```cpp void CoUninitialize(); ``` 以下是使用示例: ```cpp #include <windows.h> #include <iostream> int main() { HRESULT hr = CoInitializeEx(NULL, COINIT_MULTITHREADED); if (SUCCEEDED(hr)) { std::cout << "COM initialized successfully." << std::endl; // 这里可以进行 COM 相关操作 // 反初始化 COM CoUninitialize(); } else { std::cout << "COM initialization failed." << std::endl; } return 0; } ``` ### `__vfptr` 相关输出信息的含义 `__vfptr` 是 C++虚函数表指针(Virtual Function Table Pointer)的编译器内部名称。在 C++ 里,当一个类包含虚函数时,编译器会为该类创建一个虚函数表(VTable),这个表存储了类中所有虚函数的地址。每个包含虚函数的类的对象都会有一个隐藏的成员变量 `__vfptr`,它指向该类对应的虚函数表。 `__vfptr 0x00007ff608fac610 {MFCApplication1.exe!void(* _com_error::`vftable'[1])()} {0x00007ff608f81668 {MFCApplication1.exe!_com_error::`vector deleting destructor'(unsigned int)}} void * *` 这段输出信息的含义如下: - `__vfptr`:表明这是一个虚函数表指针。 - `0x00007ff608fac610`:是 `__vfptr` 实际存储的地址,也就是虚函数表的地址。 - `{MFCApplication1.exe!void(* _com_error::`vftable'[1])()}`:指出这个虚函数表属于 `_com_error` 类,并且该虚函数表的第一个元素是一个指向返回值为 `void`、无参数的函数指针数组。 - `{0x00007ff608f81668 {MFCApplication1.exe!_com_error::`vector deleting destructor'(unsigned int)}}`:意味着虚函数表的第一个元素指向 `_com_error` 类的 `vector deleting destructor` 函数,该函数接受一个 `unsigned int` 类型的参数。
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HHT0506

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值