C++虚函数内存对象模型
面向对象的精彩都是通过多态引起的,通过多态,我们可以完成抽象和封装,并且做到针对抽象编程,给项目的扩展带来了很多的便利。
在C++中多态的实现有两种方式:
- 重载。
- 继承。
重载其实并不能算作完美的面向对象的多态,继承才是。我们知道继承的多态主要依赖虚函数来实现,那么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
-
内存结构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;
};
那么对于这种场景,内存布局应该是怎么样的呢?我们先猜测结构是这样的
由于是多继承:
- 每个基类都有一个虚拟指针。
- 每个基类都有虚函数表。
- 子类替换所有虚函数表中的函数。
调试器中的结构如下:
跟我们的猜测是一致的,因此在多继承情况下,我们可以得出如下结论:
- 每个继承的基类,如果有自己的虚函数指针和虚函数表,那么都继承到子类。
- 子类继承的时候,编译器需要修改所有的虚函数表。
5. 总结
上述测试和验证都是在VS编译器中进行的,当然每个编译器的实现可能都会有自己的不同,如果和上述内存结构不同,也是说的通的。
总体来说,C++对于内存布局的操作按照如下规则进行:
- 易于转换(例如,子类指针转到到基类指针)。
- 节省内存(无论多少层次的基础,只有一个虚函数表)。
- 多继承,多个虚函数表。
来源:CSDN
作者:xiangbaohui
链接:https://blog.csdn.net/xiangbaohui/article/details/103889029