c++ 虚函数内存浅析(一)
一:引子
c++和其他面向对象的语言一样,都有多态,封装,继承的特性,多态主要靠重载和覆盖来实现。
如果用我自己的理解来对多态进行定义,那么“根据不同的上下文语境,使用不同的同名函数
需求的一种方法。”这样的说法可能更容易理解一些。虚函数是实现重载和覆盖的重要手段,
那么对于虚函数,编译器做了哪些工作;以及代码在cpu上到底是如何实现的呢?
二:正文
简单模式下的内存情景
- 构造,析构函数顺序
- 测试代码
类的继承关系如下:
- 测试代码
class BaseTestA
{
public:
BaseTestA()
{
m_countA = 0xa0;
printf("BaseTestA in \n");
}
~BaseTestA()
{
printf("BaseTestA out \n");
}
void PrintFun()
{
printf("BaseTestA \n");
}
virtual void virPrint()
{
printf("Base virtual print A \n");
}
int m_countA;
};
// 单线继承
class TestB : public BaseTestA
{
public:
TestB()
{
m_countB = 0xb0;
printf("TestB in \n");
}
~TestB()
{
printf("TestB out \n");
}
virtual void PrintFun()
{
printf("TestB \n");
}
virtual void virPrint()
{
printf("virPrint print B \n");
}
virtual void virRun()
{
printf("virRun print B \n");
}
int m_countB;
};
执行测试代码:
void test()
{
BaseTestA *testA = new TestB;
testA->PrintFun();
testA->virPrint();
TestB testB;
testB.PrintFun();
}
int _tmain(int argc, _TCHAR* argv[])
{
test();
// 等待结果查看
char c = getchar();
return 0;
}
执行顺序: 父类的构造函数->子类的构造函数; 子类的析构函数->父类的析构函数。
简单模式下的顺序比较简单,有兴趣的可以用od或者windbg详细跟踪下一个子类的建立与消亡的过程。
在构造函数的跟踪过程中会发现:
1. 每一个类都会有一个虚函数表,对每一个类的构造时,都会将+0偏移的位置用虚函数表指针写入。
2. 子类的虚函数表和父类的虚函数表中,同名的函数在各自的虚函数表中的偏移是相同的。
虚函数在调用时,是this调用方式,一般将ecx作为this指针进行寄存器传参,而且虚函数在调用寻址时都是从虚函数表中的偏移获取所得如:
mov ecx,eax
call [eax+0x68]
内存布局
简单情形下的内存布局也比较简单,在这里发图说明:
成员变量和虚函数的顺序都是 父类在上,依次往下子类的。
如果父类和子类有虚函数是覆盖或者重载实现的,那么子类的虚函数会覆盖掉父类的同名虚函数。
这个是在编译器自动完成的,也就是说,如果一个exe生成了,那么每一个类的虚函数表都是固定值。
本文探讨C++中虚函数的内存布局,包括构造和析构函数的顺序,以及虚函数表的创建。在简单模式下,每个类都有自己的虚函数表,子类构造时会更新表指针,且同名虚函数在不同表中的偏移相同。调用虚函数时通过虚函数表寻址。文章附带内存布局图解,展示成员变量和虚函数的排列方式。
1750

被折叠的 条评论
为什么被折叠?



