C++的虚函数 一,让我们进入内存——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和——。。。。。。。。。。。。。。。。。。。。。。。。。。。。。指向一个一维数组首地址的指针,这是个什么东东咧?且听我慢慢到来。 二,掀起你的盖头来,让我看看你的脸。为了达到后期联编的目的,VC编译器通过一个表,在执行期间接地调用了实际上需要调用的函数(注意是“间接”),这个表可称为“虚拟函数地址表”(在很多影印版的图书中常称之为vtable),每个类中含有虚拟函数的对象,编译器都会为它们指定一个虚拟函数地址表,虚拟函数地址表是个函数指针数组,保存在数据区,它由此类对象所共用(静态)。此外,编译器当然也会为它加上一个成员变量,一个指向自己的“虚拟函数地址表”的指针(常称之为vptr),并且放在了对象的首地址上。每一个由此类分配出的对象,都有这么个vptr,每当我们通过这个对象调用虚拟函数时,实际上是通过vptr找到vtable,再通过偏移量找出真正的函数地址。奥妙在于这个vtable以及这种间接调用方式,vtable是按照类中虚拟函数声明的顺序,一一填入函数地址。派生类会继承基类的vtable(当然还有其他可以继承的成员),当我们在派生类里修改了虚拟函数时,派生类的vtable中的内容也被修改,表中相应的元素不在是基类的函数地址,而是派生类的函数地址。好了,到此为止,虚函数的机制已经讲完了。如果对我的言论还抱有怀疑态度的话,我们继续。 三,现在可以下手了,做掉它。先看阅读以下一段小程序,请务必保证能看懂,并能分析出正确结果://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 ; b10012FF64> 00000002 ; b20012FF68> 00000003 ; b30012FF6C> 00426048 ; CRect aRect;0012FF70> 00000001 ; b10012FF74> 00000002 ; b20012FF78> 0042601C ; CShape aShape;0012FF7C> 00000001 ; b1以下是三个对象vtable的内容(前面是virtual void play()的地址,后面是virtual void display()的地址):00426064> 37 10 40 00 50 10 40 0000426048> 37 10 40 00 55 10 40 000042601C> 37 10 40 00 5F 10 40 00以下是代码,for循环和循环体内的反汇编:004010E9> JMP SHORT SHAPE.004010F4004010EB> MOV EAX,DWORD PTR SS:[EBP-34]004010EE> ADD EAX,1004010F1> MOV DWORD PTR SS:[EBP-34],EAX ;i++004010F4> CMP DWORD PTR SS:[EBP-34],3 ; 循环次数的控制,i<3004010F8> JGE SHORT SHAPE.00401124 ; 关键,寻址得到 &pShape004010FA> 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,ESP0040110C> CALL DWORD PTR DS:[EDX+4] ; 调用虚拟的成员函数0040110F> CMP ESI,ESP00401111> CALL SHAPE.__chkesp ; 收拾残局^_^; 寻址得到 &pShape00401116> 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

评论