C++虚函数内存对象模型

余生长醉 提交于 2020-01-11 02:02:46

C++虚函数内存对象模型

面向对象的精彩都是通过多态引起的,通过多态,我们可以完成抽象和封装,并且做到针对抽象编程,给项目的扩展带来了很多的便利。

在C++中多态的实现有两种方式:

  1. 重载。
  2. 继承。

重载其实并不能算作完美的面向对象的多态,继承才是。我们知道继承的多态主要依赖虚函数来实现,那么C++底层是如何实现虚函数的呢?本文来深度探索一下C++基于虚函数的多态对象模型。

1. 简单继承

在这个场景我们来看一下一个最简单的继承关系:
在这里插入图片描述
代码实现如下

class CLevel1
{
public:
	CLevel1() {}
	virtual ~CLevel1() {}
	virtual void Fun1()
	{
		std::cout << "CLevel1 Fun1" << std::endl;
	}
private:
	int m_Data1 = 100;
};

class CLevel2 : public CLevel1
{
public:
	CLevel2() {}
	virtual ~CLevel2() {}
	virtual void Fun1() override
	{
		std::cout << "CLevel2 Fun1" << std::endl;
	}
private:
	int m_Data2 = 200;
};

这是一个最基本的继承关系,对于这种继承,大家使用的也是最多的,这种情况大家都知道使用的是一个虚表来实现的,测试代码如下:

void ClassObj()
{
	CLevel2* p2 = new CLevel2();
	CLevel1* p1 = p2;
	CLevel1* p11 = new CLevel1();
}

具体结构如下:

0:000> dt p1
Local var @ 0xeff908 Type CLevel1*
0x011d6338 
   +0x000 __VFN_table : 0x00a97b54 
   +0x004 m_Data1          : 0n100

0:000> dt p2
Local var @ 0xeff90c Type CLevel2*
0x011d6338 
   +0x000 __VFN_table : 0x00a97b54 
   +0x004 m_Data1          : 0n100
   +0x008 m_Data2          : 0n200

0:000> dt p11
Local var @ 0xeff904 Type CLevel1*
0x011d5958 
   +0x000 __VFN_table : 0x00a97b34 
   +0x004 m_Data1          : 0n100

虚表指针__VFN_table放在最前头,然后后面是相关的地址,视图如下:

在这里插入图片描述
内存示意图如下:
在这里插入图片描述

这种内存结构非常简单,学习过C++的基本都知道这种内存结构,并且面试的时候这个是C++必面知识点,下面我们针对这种场景进行一些扩展。

2. 简单继承后子类新增虚函数

上述的场景比较简单,我们稍微复杂一点这个流程,当我们扩展CLevel2的虚函数,那么新扩展之后的数据结构是什么呢?如下是扩展之后的结构:
在这里插入图片描述

class CLevel2 : public CLevel1
{
public:
	CLevel2() {}
	virtual ~CLevel2() {}
	virtual void Fun1() override
	{
		std::cout << "CLevel2 Fun1" << std::endl;
	}
	virtual void Fun2() override
	{
		std::cout << "CLevel2 Fun2" << std::endl;
	}
private:
	int m_Data2 = 200;
};

这里我们对于CLevel2有两种猜想:

  1. 内存结构1
    在这里插入图片描述

  2. 内存结构2:
    在这里插入图片描述

那么真实的内存布局是什么样的呢?我们用调试器看下结构:
在这里插入图片描述

我们可以看到使用的内存结构2的模型。

那么Fun2是否真实在这个函数表中呢?看下虚函数表

0:000> dt p1
Local var @ 0xeffe7c Type CLevel1*
0x00f37030 
   +0x000 __VFN_table : 0x00587b54 
   +0x004 m_Data1          : 0n100

0:000> dds 0x00587b54 L3
00587b54  005813a7 CPPDemo!ILT+930(??_ECLevel2UAEPAXIZ)
00587b58  00581249 CPPDemo!ILT+580(?Fun1CLevel2UAEXXZ)
00587b5c  00581415 CPPDemo!ILT+1040(?Fun2CLevel2UAEXXZ)  //多了一个函数

0:000> u 00581415 
CPPDemo!ILT+1040(?Fun2CLevel2UAEXXZ):
00581415 e946070000      jmp     CPPDemo!CLevel2::Fun2 (00581b60)  //函数汇编

因此就算我们使用p1 的指针,其虚拟表中还是存在不是自己的函数的,编译器使用这种内存布局的好处是减少内存大小(减少虚拟表的数目)

3. 继承的虚函数并非最初子类

我们针对类关系稍微再做相关调整:
在这里插入图片描述

class CLevel0
{
public:
	CLevel0() {}
	~CLevel0() {}
private:
	int m_Data0 = 10;
};
class CLevel1 : public CLevel0
{
public:
	CLevel1() {}
	virtual ~CLevel1() {}
	virtual void Fun1()
	{
		std::cout << "CLevel1 Fun1" << std::endl;
	}
private:
	int m_Data1 = 100;
};

class CLevel2 : public CLevel1
{
public:
	CLevel2() {}
	virtual ~CLevel2() {}
	virtual void Fun1() override
	{
		std::cout << "CLevel2 Fun1" << std::endl;
	}
private:
	int m_Data2 = 200;
};

这个例子的目的是为了验证虚函数表应该放在哪个地方

我们先猜测结构应该如下:
在这里插入图片描述
因为这种结构的好处是方便赋值到CLevel0的指针。调试器中,结构如下:
在这里插入图片描述

证明猜测是正确的。

4. 多继承

我来处理一种最复杂的场景,就是多继承,类层次关系如下:
在这里插入图片描述

class CLevel1
{
public:
	CLevel1() {}
	virtual ~CLevel1() {}
	virtual void Fun1()
	{
		std::cout << "CLevel1 Fun1" << std::endl;
	}
private:
	int m_Data1 = 100;
};

class CLevel11
{
public:
	CLevel11() {}
	virtual ~CLevel11() {}
	virtual void Fun1()
	{
		std::cout << "CLevel11 Fun1" << std::endl;
	}
private:
	int m_Data11 = 110;
};
class CLevel2 : public CLevel1, public CLevel11
{
public:
	CLevel2() {}
	virtual ~CLevel2() {}
	virtual void Fun1() override
	{
		std::cout << "CLevel2 Fun1" << std::endl;
	}
private:
	int m_Data2 = 200;
};

那么对于这种场景,内存布局应该是怎么样的呢?我们先猜测结构是这样的
在这里插入图片描述
由于是多继承:

  1. 每个基类都有一个虚拟指针。
  2. 每个基类都有虚函数表。
  3. 子类替换所有虚函数表中的函数。

调试器中的结构如下:
在这里插入图片描述

跟我们的猜测是一致的,因此在多继承情况下,我们可以得出如下结论:

  1. 每个继承的基类,如果有自己的虚函数指针和虚函数表,那么都继承到子类。
  2. 子类继承的时候,编译器需要修改所有的虚函数表。

5. 总结

上述测试和验证都是在VS编译器中进行的,当然每个编译器的实现可能都会有自己的不同,如果和上述内存结构不同,也是说的通的。

总体来说,C++对于内存布局的操作按照如下规则进行:

  1. 易于转换(例如,子类指针转到到基类指针)。
  2. 节省内存(无论多少层次的基础,只有一个虚函数表)。
  3. 多继承,多个虚函数表。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!