今天面试被人问了:虚表模型。一听见模型,这俩字,我就一下子就蒙了。像我这种不爱看书的人,回答这种问题,真不是那么容易啊。也不知道,面试管要问什么。就拿爱情公寓里定义就业一样:就业,16周岁以上,具有劳动能力的公民,是指具有劳动能力的公民,依法从事某种有报酬或劳动收入的社会活动....。这不就是背课本嘛。
以下是,我在底层对虚表的理解。
class A
{
int nA;
virtual void a()
{
};
virtual int bb()
{
return 0;
};
public:
~A()
{
};
};
class B :public A
{
public:
B::B(int)
{
}
virtual void a()
{
}
virtual void aa()
{
}
virtual int b()
{
return 0;
};
~B()
{
};
};
int _tmain(int argc, _TCHAR* argv[])
{
B b(1);
A a();
A *pA = &b;
//B *pB = &a; //这个无法编译
B *pB = (B *)&a; //这个可以编译
pB->aa(); //这运行就会出错
return 0;
}
以下是B的构造函数的反汇编
.text:004114A0 ; void __thiscall B__B(B *this, int __formal)
.text:004114A0 ??0B@@QAE@H@Z proc near ; CODE XREF: .text:004110FFj
.text:004114A0 ; B::B(int)j
.text:004114A0
.text:004114A0 var_CC = byte ptr -0CCh
.text:004114A0 this = dword ptr -8
.text:004114A0 __formal = dword ptr 8
.text:004114A0
.text:004114A0 push ebp
.text:004114A1 mov ebp, esp
.text:004114A3 sub esp, 0CCh
.text:004114A9 push ebx
.text:004114AA push esi
.text:004114AB push edi
.text:004114AC push ecx
.text:004114AD lea edi, [ebp+var_CC]
.text:004114B3 mov ecx, 33h
.text:004114B8 mov eax, 0CCCCCCCCh
.text:004114BD rep stosd
.text:004114BF pop ecx
.text:004114C0 mov [ebp+this], ecx
.text:004114C3 mov ecx, [ebp+this] ; this
.text:004114C6 call A::A(void) 调用A的构造函数
.text:004114CB mov eax, [ebp+this]
.text:004114CE mov dword ptr [eax], offset ??_7B@@6B@ ; const B::`vftable' [this] = virtualtable
.text:004114D4 mov eax, [ebp+this]
.text:004114D7 pop edi
.text:004114D8 pop esi
.text:004114D9 pop ebx
.text:004114DA add esp, 0CCh
.text:004114E0 cmp ebp, esp
.text:004114E2 call j___RTC_CheckEsp
.text:004114E7 mov esp, ebp
.text:004114E9 pop ebp
.text:004114EA retn 4
.text:004114EA ??0B@@QAE@H@Z endp
以上的大体意思如下。这里的代码是编译器自动加上去了。这说明了,构造函数是没有虚函数一说,也没有必要。
B::B()
{
A::A();
[this] = B.VirtualTbale;
}
首先,A::A() 里也会初始化一个虚表,如果,构造函数会有虚函数一说的话,那基本就乱了。这也就是先有蛋还是先有鸡一说了。虚表要,在构造函数里初始化,而构造函数会调用到函数里的函数。
但是虑析构函数就是存在的。以下会说明。还是先看虚表是如何结构。
虚表在什么地方呢。[this] = B.VirtualTable 可以看的出。它是在类的前几个字节存放的指针。(32位系统,4字节,64就是8字节)
A 的结构就是
struct
{
PVOID pVairtualTable; //是一个指针。这就是虚表
int nA;
};
首先,A::A() 里也会初始化一个虚表,如果,构造函数会有虚函数一说的话,那基本就乱了。这也就是先有蛋还是先有鸡一说了。虚表要,在构造函数里初始化,而构造函数会调用到函数里的函数。但是虑析构函数就是存在的。
.rdata:00415740 ??_7B@@6B@ dd offset j_?a@B@@UAEXXZ ; B::a(void)
.rdata:00415744 dd offset j_?aa@A@@EAEHXZ ; A::aa(void)
.rdata:00415748 dd offset j_?bb@B@@UAEXXZ ; B::bb(void)
.rdata:0041574C dd offset j_?b@B@@UAEHXZ ; B::b(void)
.rdata:00415750
..rdata:00415758 ??_7A@@6B@ dd offset j_?a@A@@EAEXXZ ; A::a(void)
.rdata:0041575C dd offset j_?aa@A@@EAEHXZ ; A::aa(void)
一眼就可以看出,B虚表里有4个函数,A里有二个。因为,B继承了A,所以,所以,aa() 表会在B里,但是A.a 由于B自己又定义了一个,所以,那个就会是B::a了。
还有其它两个是自己定义的。
现在看这个,B *pB = &a; 这是编译不过去的。为什么,因为,a里的虚表是没有,B里面所有的。如果编译,那么,pB->bb() 这个调用就,这个就会有问题。
下面说明虚析构函数。
格式是和其它的虚函数一样。也会在虚表里。但其是作为特别存在的。不是说它能力,而是说,它的作用
例如:A *pA = new B; delete pA; 如果没有虚析函数,那它只能调用 A的析函数。否则,它就会调用 A和B的析函数。
这就很有用。使用过MFC那个分屏的就知道。CSplitterCtrl.CreateView(int row,int col,CRuntimeClass *pViewClass)创建一个分屏。这个是通过内容进行new 一个类的。那自己无需,也无法delete.因为它是内部进行删除了。如果,没有虚析函数,那它就没法调用析构派生类。