正文

C++的虚函数2007-11-18 11:06:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/lhlvc/31022.html

分享到:

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             ; 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
 

阅读(2771) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册