虚函数?!
虚拟+函数?!听起来很恐怖的样子。如果你知道使用微波 炉煎鸡蛋和使用煤气炉煎鸡蛋的方法和步骤绝对不一样的话,就应该知道为什么需要虚拟函数和在什么情况下使用虚拟函数,这些都是课堂上的内容,相信大家已经滚瓜烂熟,我也就不罗嗦了。我在这里是要和大家讨论虚拟函数的技术内幕——后期联编(Late binding),但是,如果你对以上我所提到的还没有完全的领悟的话,则千万不要阅读本帖,以防走火入魔,切记切忌。
一,让我们进入内存——I will come back(阿诺的口头禅)
首先,我觉得了解一个含有虚拟函数的类在内存中的结构是有必要的。
假设一个这样的类:
class CShape
{
int b1
public:
void MyTest()
{
cout << "CShape::MyTest \n"
}
}
在栈区,它仅仅只是占据了四个字节,用于存放成员数据——b1。
奇 怪,那么它的成员函数在那里呢?对于普通的成员函数,编译器采取的是“名字粉碎法”,对于VC++6.0,它将CShape::MyTest()编修改 为:“?MyTest@CTestA@@QAEXXZ”,真是个奇怪的名字,但是在这个名字中却保存了重要的信息,比如所属类,参数类型等,具体的大家可 以查查相关资料,不好意思,我忘记了。
现在我们讨论虚拟函数, 假设另外的一个类:
class CShape_V
{
int b1
public:
virtual void play()
{
cout << "CShape::play \n"
}
virtual void display()
{
cout <<b1<< "Shape \n"
}
}
在栈区,它占据了八个字节,用于存放成员数据b1和——。。。。。。。。。。。。。。。。。。。。。。。。。。。。。指向一个一维数组首地址的指针,这是个什么东东咧?且听我慢慢到来。
三,现在可以下手了,做掉它。
先看阅读以下一段小程序,请务必保证能看懂,并能分析出正确结果:
//vTest.cpp
#include <iostream.h>
//
class CShape
{
int b1
public:
CShape():b1(1){}
void MyTest()
{
cout << "CShape::MyTest \n"
}
virtual void play()
{
cout << "CShape::play \n"
}
virtual void display()
{
cout <<b1<< "Shape \n"
}
}
//
class CRect : public CShape
{ int b2
public:
CRect():b2(2){}
void MyTest()
{
cout << "CRect::MyTest \n" }
void display()
{
cout <<b2<< "Rectangle \n"
}
}//
class CSquare : public CRect
{
int b3
public:
CSquare():b3(3){}
void MyTest()
{
cout << "CSquare::MyTest \n"
}
void display()
{
cout <<b3<< "Square \n"
}
}
//
void main()
{
CShape aShape
CRect aRect
CSquare aSquare
CShape* pShape[3] = { &aShape,
&aRect,
&aSquare }
for (int i=0 i< 3 i++)
{
pShape[i]->display()
pShape[i]->MyTest()
}
}
以下是上面那个程序(vTest.cpp)里for循环和循环体中的内存结构,代码的汇编,和一些注释,我能证明的只有这些了(相信我的同志就可以不用看啦^_^)。
以下是栈:
0012FF4C> 00000000 int i //(循环体内的定义)
(责任编辑:科锐软件教育机构)0012FF50> 0012FF78 CShape* pShape[0] 0012FF54> 0012FF6C pShape[1] 0012FF58> 0012FF5C pShape[2] 0012FF5C> 00426064 CSquare aSquare 0012FF60> 00000001 b1 0012FF64> 00000002 b2 0012FF68> 00000003 b3 0012FF6C> 00426048 CRect aRect 0012FF70> 00000001 b1 0012FF74> 00000002 b2 0012FF78> 0042601C CShape aShape 0012FF7C> 00000001 b1 以下是三个对象vtable的内容(前面是virtual void play()的地址,后面是virtual void display()的地址): 00426064> 37 10 40 00 50 10 40 00 00426048> 37 10 40 00 55 10 40 00 0042601C> 37 10 40 00 5F 10 40 00 以下是代码,for循环和循环体内的反汇编: 004010E9> JMP SHORT SHAPE.004010F4 004010EB> MOV EAX,DWORD PTR SS:[EBP-34] 004010EE> ADD EAX,1 004010F1> MOV DWORD PTR SS:[EBP-34],EAX i++ 004010F4> CMP DWORD PTR SS:[EBP-34],3 循环次数的控制,i<3 004010F8> JGE SHORT SHAPE.00401124 关键,寻址得到 &pShape 004010FA> MOV ECX,DWORD PTR SS:[EBP-34] 004010FD> MOV ECX,DWORD PTR SS:[EBP+ECX*4-30] 00401101> MOV EDX,DWORD PTR SS:[EBP-34] 00401104> MOV EAX,DWORD PTR SS:[EBP+EDX*4-30] 关键,得到函数表的首地址 00401108> MOV EDX,DWORD PTR DS:[EAX] 0040110A> MOV ESI,ESP 0040110C> CALL DWORD PTR DS:[EDX+4] 调用虚拟的成员函数 0040110F> CMP ESI,ESP 00401111> CALL SHAPE.__chkesp 收拾残局^_^ 寻址得到 &pShape 00401116> MOV EAX,DWORD PTR SS:[EBP-34] 00401119> MOV ECX,DWORD PTR SS:[EBP+EAX*4-30] 0040111D> CALL SHAPE.00401073 调用普通的成员函数 00401122> JMP SHORT SHAPE.004010EB |