浅析C++中虚函数的调用及对象的内部布局1 (来自:lizhe1985)
在我那篇《浅析C++中的this指针》中,我通过分析C++代码编译后生成的汇编代码来分
析this指针的实现方法。这次我依然用分析C++代码编译后生成的汇编代码来说明C++中
虚函数调用的实现方法,顺便也说明一下C++中的对象内部布局。下面所有的汇编代码都
是用VC2005编译出来的。虽然,不同的编译器可能会编译出不同的结果,对象的内部布
局也不尽相同;但是,只要是符合C++标准的编译器,编译结果和对象的内部布局应该是
大同小异。
首先,是一个有着简单继承关系的两个类:
class CBase
{
public:
virtual void VFun1() = 0;
virtual void VFun2() = 0;
void Fun1();
};
// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CBase::Fun1()
{
}
class CDerived : public CBase
{
public:
virtual void VFun1();
virtual void VFun2();
void Fun2();
private:
int m_iValue1;
int m_iValue2;
};
// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CDerived::VFun1()
{
}
// 这里仅仅是为了生成函数的汇编代码,因此函数体为空
void CDerived::VFun2()
{
}
// 这里是为了分析对象的内部布局,因此仅仅是给成员变量赋值
void CDerived::Fun2()
{
m_iValue1 = 13;
m_iValue2 = 13;
}
现在用下面的代码来调用成员函数:
CDerived derived;
// 用对象调用虚函数
derived.VFun1();
derived.VFun2();
// 用对象调用非虚函数
derived.Fun1();
derived.Fun2();
// 用指向派生类的基类的指针调用虚函数,实现多态
CBase *pTest = &derived;
pTest->VFun1();
pTest->VFun2();
下面就是用VC2005编译上面的代码后生成的汇编代码:
CDerived derived;
0041195E lea ecx,[derived]
00411961 call CDerived::CDerived (411177h)
// 代码段1
derived.VFun1();
00411966 lea ecx,[derived]
00411969 call CDerived::VFun1 (411078h)
derived.VFun2();
0041196E lea ecx,[derived]
00411971 call CDerived::VFun2 (4111B8h)
derived.Fun1();
00411976 lea ecx,[derived]
00411979 call CBase::Fun1 (411249h)
derived.Fun2();
0041197E lea ecx,[derived]
00411981 call CDerived::Fun2 (4111BDh)
// 代码段2
CBase *pTest = &derived;
00411986 lea eax,[derived]
00411989 mov dword ptr [pTest],eax
pTest->VFun1();
0041198C mov eax,dword ptr [pTest] // 行1
0041198F mov edx,dword ptr [eax] // 行2
00411991 mov esi,esp
00411993 mov ecx,dword ptr [pTest]
00411996 mov eax,dword ptr [edx] // 行3
00411998 call eax // 行4
0041199A cmp esi,esp
0041199C call @ILT+495(__RTC_CheckEsp) (4111F4h)
pTest->VFun2();
004119A1 mov eax,dword ptr [pTest]
004119A4 mov edx,dword ptr [eax]
004119A6 mov esi,esp
004119A8 mov ecx,dword ptr [pTest]
004119AB mov eax,dword ptr [edx+4] // 行5
004119AE call eax
004119B0 cmp esi,esp
004119B2 call @ILT+495(__RTC_CheckEsp) (4111F4h)
通过对代码段1的观察我们可以发现:通过对象调用类的虚成员函数和调用非虚成员函数
是相同的(对调用成员函数的汇编代码的分析可以看我的那篇《浅析C++中的this指针》
)。也就是说,用对象是无法实现多态的。
下面主要来分析实现多态的代码段2。
行1、将pTest指针指向的地址前2个字(4个字节,也就是32位系统中一个指针的大小)
的内容当成一个指针放到eax寄存器中
行2、将eax寄存器中的指针的值放入edx寄存器
行3、将dex寄存器中的指针的值放入eax寄存器
行4、调用eax寄存器指向的函数
这样分析似乎对怎样调用对象derived的虚函数VFun1()并不是很清楚。那么我们先来看
下面的这张图:
这张图是一个假设的对象derived在内存中的内部布局图。指针pTest指向对象derived,
而对象derived的前4个字节是一个虚表指针,指向虚函数表。
看着这张图再来分析上面的汇编代码就会清晰很多:
行1、取得虚表指针值放入eax寄存器中
行2、取得虚表指针的值放入edx寄存器中
行3、取得虚表指针指向的地址的值(也就是VFun1)放入eax寄存器中
行4、调用eax寄存器指向的函数
行5证明了上面图中对虚函数表的假设。第二个虚函数VFun2()的地址就是通过在第一虚
函数VFun1()的地址加4(32位系统中一个指针的大小)而得到的。
通过上面的分析,可以得出C++中虚函数的调用方法:首先,取得对象中的虚表指针;然
后,通过虚表指针找到相应的虚表;最后,通过在虚表内的偏移量找到相应的函数来调
用。
上面的是转帖,对于虚函数有一个基本的认识,下面开始具体的理论的讲解。
如果不是虚函数,那么我们对象一旦确定,我们的对象里面的成员函数的指针就确定下来了,没有必要需要额外的空间去存放函数指针。
如果一个函数声明为虚函数,那么这个函数最终的实现是未定的,那么就必须要在每一个对象空间里面放这样的一块空间来存放函数指针。这就是我们通常说的虚函数表。
上图就是我们父对象与子对象的存放规则。在内存上面是邻近的。对于一个32位的系统,如果一个类有虚函数,那么在这个类的开始4个字节就存放的虚函数表存放的地址。
还有一个切片的问题,把一个包含有虚函数的类作为指针传递的时候,在传递参数的时候,就对类进行了剪切,必须是按照指针或者引用来传递,而不是值的传递。
在C++中很少看到函数参数是通过值来传递的。如果按照值来传递,那么我们的之类的虚函数就会被剪切掉。
虚析构函数:
因为我们析构函数,如果不为虚函数。我们delete 一个基类的指针的时候,他只会调用这个积累的析构,但是如果这个指针指向的其实是我们的一个具体的派生类,那么具体派生类就不会调用其析构,导致代码的问题。
一般来说,如果你的类中任何一个函数是虚函数,那么你的析构就要为虚。
虚函数,到调用一个函数为虚的时候,这个时候系统做的事情是首先查找这个虚函数表,看这儿函数又没有映射到其他的地方。