C++工具机关及多态实现之带虚函数的类
副标题#e#
假如类中存在虚函数时,环境会奈何呢?我们知道当一个类中有虚函数时,编译器会为该类发生一个虚函数表,并在它的每一个工具中插入一个指向该虚函数表的指针,凡是这个指针是插在工具的起始位置。所谓的虚函数表实际就是一个指针数组,个中的指针指向真正的函数起始地点。我们来验证一下,界说一个无成员变量的类C040,内含一个虚函数。
struct C040
{
virtual void foo() {}
};
运行如下代码打印它的巨细及工具中的内容。
PRINT_SIZE_DETAIL(C040)
功效为:
The size of C040 is 4
The detail of C040 is 40 b4 45 00
公然它的巨细为4字节,即含有一个指针,指针指向的地点为0x0045b440。
同样再界说一个空类C050,派生自类C040。
struct C050 : C040
{};
由于虚函数会被担任,且维持为虚函数。那么类C050的工具中同样应该含有一个指向C050的虚函数表的指针。
运行如下代码打印它的巨细及工具中的内容。
PRINT_SIZE_DETAIL(C050)
功效为:
The size of C050 is 4
The detail of C050 is 44 b4 45 00
公然它的巨细也为4字节,即含有一个指向虚函数表(后称虚表)的指针(后称虚表指针)。
虚表是类级此外,类的所有工具共享同一个虚表。我们可以生成类C040的两个工具,然后通过调查工具的地点、虚表指针地点、虚表地点、及虚表中的条目标值(即所指向的函数地点)来举办验证。
运行如下代码:
C040 obj1, obj2;
PRINT_VTABLE_ITEM(obj1, 0, 0)
PRINT_VTABLE_ITEM(obj2, 0, 0)
功效如下:
obj1 : objadr:0012FDC4 vpadr:0012FDC4 vtadr:0045B440 vtival(0):0041D834
obj2 : objadr:0012FDB8 vpadr:0012FDB8 vtadr:0045B440 vtival(0):0041D834
(注:第一列为工具名,第二列(objadr)为工具的内存地点,第三列(vpadr)为虚表指针地点,第四列(vtadr)为虚表的地点,第五列(vtival(n))为虚表中的条目标值,n为条目标索引,从0开始。后同)
#p#副标题#e#
公然工具地点差异,虚表指针(vpadr)位于工具的起始位置,所以它的地点和工具沟通。两个工具的虚表指针指向的是同一个虚表,因此(vtadr)的值沟通,虚表中的第一条目(vtival(0))的值虽然也一样。
接下来,我们再调查类C040和从它派生的类C050的工具,这两个类各有本身的虚表,但由于C050没有重写担任自C040的虚函数,所以它们的虚表中的条目标值,即指向的虚函数的地点应该是一样的。
运行如下代码:
C040 c040;
C050 c050;
PRINT_VTABLE_ITEM(c040, 0, 0)
PRINT_VTABLE_ITEM(c050, 0, 0)
功效为:
c040 : objadr:0012FD4C vpadr:0012FD4C vtadr:0045B448 vtival(0):0041D834
c050 : objadr:0012FD40 vpadr:0012FD40 vtadr:0045B44C vtival(0):0041D834
公然这次我们可以看到固然前几列皆不沟通,但最后一列的值沟通。即它们共享同一个虚函数。
界说一个C043类,包括两个虚函数。再界说一个C071类,从C043派生,并重写担任的第一个虚函数。
struct C043
{
virtual void foo1() {}
virtual void foo2() {}
};
struct C071 : C043
{
virtual void foo1() {}
};
我们可以预推测,C043和C071各有一个包括两个条目标虚表,由于C071派生自C043,而且重写了第一个虚函数。那么这两个类的虚表的第一个条目值是差异的,而第二项应该是沟通的。运行如下代码。
C043 c043;
C071 c071;
PRINT_SIZE_DETAIL(C071)
PRINT_VTABLE_ITEM(c043, 0, 0)
PRINT_VTABLE_ITEM(c071, 0, 0)
PRINT_VTABLE_ITEM(c043, 0, 1)
PRINT_VTABLE_ITEM(c071, 0, 1)
功效为:
The size of C071 is 4
The detail of C071 is 5c b4 45 00
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(0):0041D4F1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(0):0041D811
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(1):0041DFE1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(1):0041DFE1
调查第1、2行的最后一列,即两个类的虚表的第一个条目,由于C071重写了foo1函数,所以这个值纷歧样。而第3、4行的最后一列为两个类的虚表的第二个条目,由于C071并没有重写它,所以这两个值是沟通的。和我们之间的揣摩是一致的。
接下来我们看看多重担任。界说两个类,各含一个虚函数,及一个数据成员。再从这两个类派生一个空子类。
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
struct C042
{
C042() : c_(0x02) {}
virtual void foo2() {}
char c_;
};
struct C051 : public C041, public C042
{};
运行如下代码:
PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C042)
PRINT_SIZE_DETAIL(C051)
#p#分页标题#e#
功效为:
The size of C041 is 5
The detail of C041 is 64 b3 45 00 01
The size of C042 is 5
The detail of C042 is 68 b3 45 00 02
The size of C051 is 10
The detail of C051 is 6c b4 45 00 01 68 b4 45 00 02
留意,首先我们调查C051的工具输出,发明它的巨细为10字节,这说明它有两个虚表指针,从导出的内存数据我们可以揣度,首先是一个虚表指针,然后是从C041担任的成员变量,值也是我们在C041的结构函数中赋的值0x01,然后又是一个虚表指针,再是从C042担任的成员变量,值为0x02。
为了验证,我们再运行如下代码:
C041 c041;
C042 c042;
C051 c051;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c051, 0, 0)
PRINT_VTABLE_ITEM(c051, 5, 0)
留意最后一行的第二个参数,5。它是从工具起始地点开始到虚表指针的偏移值(按字节计较),从上面的工具内存输出我们看到C041的巨细为5字节,因此C051中第二个虚表指针的起始位置距工具地点的偏移为5字节。输出的功效为:
(注:这个偏移值是通过调查而判定出来的,并不通用,并且它依赖于我们前面所说的编译器在生成代码时所用的布局成员对齐方法,我们将这个值设为1。假如设为其他值会影响工具的巨细及这个偏移值。拜见第一篇起始处的说明。下同。)
c041 : objadr:0012FB88 vpadr:0012FB88 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FB78 vpadr:0012FB78 vtadr:0045B368 vtival(0):0041D43D
c051 : objadr:0012FB64 vpadr:0012FB64 vtadr:0045B46C vtival(0):0041DF1E
c051 : objadr:0012FB64 vpadr:0012FB69 vtadr:0045B468 vtival(0):0041D43D
这下我们可以看到C051的两个虚表指针指向两个不现的虚表(第3、4行的vtadr列),而虚表中的条目标值别离便是C041和C042(即它的两个父类)的虚表条目标值(第1、3行和2、4行的vtival列的值沟通)。
为什么子类要有两个虚表,而不是将它们归并为一个。主要是在处理惩罚范例的动态转换时这种工具机关更利便调解指针,后头我们看到这样的例子。
假如子类重写父类的虚函数会怎么样?前面的类C071我们已经看到过一次了。我们再界说一个从C041和C042派生的类C082,并重写这两个父类中的虚函数,同时再增加一个虚函数。
struct C041
{
C041() : c_(0x01) {}
virtual void foo() { c_ = 0x02; }
char c_;
};
struct C042
{
C042() : c_(0x02) {}
virtual void foo2() {}
char c_;
};
struct C082 : public C041, public C042
{
C082() : c_(0x03) {}
virtual void foo() {}
virtual void foo2() {}
virtual void foo3() {}
char c_;
};
运行和上面雷同的代码:
PRINT_SIZE_DETAIL(C082)
C041 c041;
C042 c042;
C082 c082;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c082, 0, 0)
PRINT_VTABLE_ITEM(c082, 5, 0)
功效为:
The size of C082 is 11
The detail of C082 is 70 b3 45 00 01 6c b3 45 00 02 03
c041 : objadr:0012FA74 vpadr:0012FA74 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FA64 vpadr:0012FA64 vtadr:0045B368 vtival(0):0041D43D
c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(0):0041D87A
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483
公然C082的两个虚表中的条目值都和父类的纷歧样了(vtival列),指向了重写后的新函数地点。调查C082的巨细和工具内存,我们可以知道它并没有为新界说的虚函数foo3生成新的虚表。那么foo3的函数地点到底是加到了类的第一个虚表,照旧第二个虚表中?在调试状态下,我们在“局部变量”窗口中展开c082工具。我们可以看到两个虚表及个中的条目,但两个虚表都只能看到第一个条目。这应该是VC7.1IDE的一个小BUG。看来我们只有另想步伐来验证。我们先把两个虚表中的第二个条目位置上的值打印出来。运行如下代码。
PRINT_VTABLE_ITEM(c082, 0, 1)
PRINT_VTABLE_ITEM(c082, 5, 1)
功效如下:
c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(1):0041D32F
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(1):0041D87A
然后我们挪用一下foo3函数:
c082.foo3();
查察它的汇编代码:
004225F3 lea ecx,[ebp+FFFFFB74h]
004225F9 call 0041D32F
第2条call指令后的地点就是foo3的函数地点了(实际上是一个跳转指令),比较前面的输出我们就可以知道,子类新界说的虚函数对应的虚表条目插手到了子类的第一个虚表中,并位于担任自父类的虚表条目之后。