zoukankan      html  css  js  c++  java
  • 在VC6.0中虚函数的实现原理

    讨论虚拟函数的技术内幕——后期联编(Late binding),

       一,进入内存
    首先,一个含有虚拟函数的类在内存中的结构。
    假设一个这样的类:

    class CShape
    {
            int b1;
    public:
            void MyTest()
            {
                     cout << "CShape::MyTest /n";
            }
    };
    在栈区,它仅仅只是占据了四个字节,用于存放成员数据——b1。 sizeof(CShape)
    奇怪,那么它的成员函数在那里呢?在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和指向一个一维数组首地址的指针。

    二,vtable

    为了达到后期联编的目的,VC编译器通过一个表,在执行期间接地调用了实际上需要调用的函数(注意是“间接”),这个表可称为“虚拟函数地址表”(在很多影印版的图书中常称之为vtable),每个类中含有虚拟函数的对象,编译器都会为它们指定一个虚拟函数地址表,虚拟函数地址表是个函数指针数组,保存在数据区,它由此类对象所共用(静态)。此外,编译器当然也会为它加上一个成员变量,一个指向自己的“虚拟函数地址表”的指针(常称之为vptr),并且放在了对象的首地址上
    每一个由此类分配出的对象,都有这么个vptr,每当我们通过这个对象调用虚拟函数时,实际上是通过vptr找到vtable,再通过偏移量找出真正的函数地址
    奥妙在于这个vtable以及这种间接调用方式,vtable是按照类中虚拟函数声明的顺序,一一填入函数地址。派生类会继承基类的vtable(当然还有其他可以继承的成员),当我们在派生类里修改了虚拟函数时,派生类的vtable中的内容也被修改,表中相应的元素不在是基类的函数地址,而是派生类的函数地址

    三,分析:
    //vTest.cpp
    #include <iostream.h>
    //--------------------------------------------
    class CShape
    {
            int b1;
    public:
          
            void MyTest(){
                 cout << "CShape::MyTest /n";
            }
            virtual void play()
            {
             cout << "CShape::play /n";
             }
            virtual void display()
            {
            cout <<b1<< "Shape /n";
            }

    };
    //--------------------------------------------
    class       int b2;
    public:
          
            void MyTest()
            {
             cout << "CRect::MyTest /n";
            }
             void display()
             {
             cout <<b2<< "Rectangle /n";
             }
    };
    //--------------------------------------------
    class       int b3;
    public:   
          
            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();
            }
    }

    =====================第二部分====================

    107:   void main()
    108:   {

    004117D0    push         ebp
    004117D1    mov          ebp,esp
    004117D3    sub          esp,74h
    004117D6    push         ebx
    004117D7    push         esi
    004117D8    push         edi
    004117D9    lea          edi,[ebp-74h]
    004117DC    mov          ecx,1Dh
    004117E1    mov          eax,0CCCCCCCCh
    004117E6    rep stos     dword ptr [edi]
    109:          CShape    aShape;       //aShape 地址0x0012ff78
    004117E8    lea          ecx,[ebp-8] ;ebp 为0x0012ff80,执行后ecx为0012ff78,
    004117EB    call         @ILT+65(ostream::operator<<) (00401046)
    110:          CRect1     aRect;       //aRect 0x0012ff6c
    004117F0    lea          ecx,[ebp-14h]   ;ebp 为0x0012ff80,执行后eex为0012ff6c,即指向aRect,也

    就是aRect的this指针,相关教材上说this一般保存在ecx。
    004117F3    call         @ILT+80(CRect::CRect) (00401055)   ;构造函数(后面详细讲解)
    111:          CSquare   aSquare; //aSquare 地址0x0012ff5c
    004117F8    lea          ecx,[ebp-24h] //ecx为0x0012ff5c,ebp为0012ff80
    004117FB    call         @ILT+60(CSquare::CSquare) (00401041) ;构造函数
    112:          CShape* pShape[3] = { &aShape,&aRect,&aSquare }; //pShape为0x0012ff50
    00411800    lea          eax,[ebp-8] //ebp=0x0012ff80;eax=0x0012ff78,即为aShape的地址
    00411803    mov          dword ptr [ebp-30h],eax;把eax发到ebp-30h的内容空间里(4个字节),

    ebp=0x0012ff80,[ebp-30]=0x0012ff50即pShape的地址。
    00411806    lea          ecx,[ebp-14h];ecx=0x0012ff6c即为aRect
    00411809    mov          dword ptr [ebp-2Ch],ecx //[ebp-2c]=0x0012ff54,即为pShape[1]
    0041180C    lea          edx,[ebp-24h]   //同理
    0041180F    mov          dword ptr [ebp-28h],edx //同上
    113:          for (int i=0; i< 3; i++)   //i地址0x0012ff4c
    00411812    mov          dword ptr [ebp-34h],0   //内存为0x0012ff80-34h=012ff4c,即i
    00411819    jmp          main+54h (00411824)    ;跳到00411824
    0041181B    mov          eax,dword ptr [ebp-34h] ;把i给eax;
    0041181E    add          eax,1                    ;eax加1
    00411821    mov          dword ptr [ebp-34h],eax ;eax再加1,上面三条指令完成i++
    00411824    cmp          dword ptr [ebp-34h],3    ;把i和3做比较
    00411828    jge          main+84h (00411854)      ;如果不小于则跳到00411854
    114:          {
    115:                 pShape[i]->display();       ;   //下面以i=1为例,aRect1

    0041182A    mov          ecx,dword ptr [ebp-34h] ;ebp-34h为i的内存,把i给ecx;
    0041182D    mov          ecx,dword ptr [ebp+ecx*4-30h] ;基址+变址寻址 ebp-30h为pShape的地址

    ,ecx(i)为变址,通过i索引数组里不同的对象;最后ecx得到了当前对象的this指针即对象的地址
    00411831    mov          edx,dword ptr [ebp-34h] ;i给edx
    00411834    mov          eax,dword ptr [ebp+edx*4-30h];eax同样得到了对象的地址,eax=0x0012ff6c
    00411838    mov          edx,dword ptr [eax] ;[eax]放着对像,对象的第一个数据是vtable的地址,

    所以[edx]里面是对象的第一个虚函数;edx=0x00429114,[edx]=0x00401050
    0041183A    mov          esi,esp ;
    0041183C    call         dword ptr [edx+4] ;[edx+4]为第二个虚函数的地址。[edx+4]=0x0040105a,虚函数的地址。间接。这个虚函数的调用:取得this指针,得vtable,根据vtable里面的地址,得出虚函数。
    0041183F    cmp          esi,esp               ;//??
    00411841    call         __chkesp (004011e0)   ;堆栈清除
    116:                 pShape[i]->MyTest();
    00411846    mov          eax,dword ptr [ebp-34h] ;i
    00411849    mov          ecx,dword ptr [ebp+eax*4-30h] ;对象地址
    0041184D    call         @ILT+25(CShape::MyTest) (0040101e) ;//调用CShape::MyTest函数,注意这2个函数的调用方法的区别
    117:          }
    00411852    jmp          main+4Bh (0041181b)   ;跳回
    118:
    119:   }

    00411854    pop          edi
    00411855    pop          esi
    00411856    pop          ebx
    00411857    add          esp,74h
    0041185A    cmp          ebp,esp
    0041185C    call         __chkesp (004011e0)
    00411861    mov          esp,ebp
    00411863    pop          ebp
    00411864    ret

    分析CRect1 aRect的构造函数:
    110:          CRect1     aRect;
    004117F0    lea          ecx,[ebp-14h]
    004117F3    call         @ILT+80(CRect::CRect) (00401055)
    函数从call 进入:跳转到0x00401055可以看到:
    00401055    jmp          CRect1::CRect1 (00411530)
    0040105A    jmp          CRect1::display (00411580)
    0040105F    int          3
    00401060    int          3
    00401061    int          3
    在跟进去:
    73:           CRect1():b2(2){};
    00411530    push         ebp
    00411531    mov          ebp,esp
    00411533    sub          esp,44h
    00411536    push         ebx
    00411537    push         esi
    00411538    push         edi
    00411539    push         ecx //堆栈保护
    0041153A    lea          edi,[ebp-44h]   //ebp=0x0012fef8 ,edi=0x0012ffb4
    0041153D    mov          ecx,11h         ;
    00411542    mov          eax,0CCCCCCCCh
    00411547    rep stos     dword ptr [edi] ;以上三条把
    00411549    pop          ecx               ;0x0012ff6c(好熟悉啊,就是aRect1的地址,this指针)
    0041154A    mov          dword ptr [ebp-4],ecx ;
    0041154D    mov          ecx,dword ptr [ebp-4]
    00411550    call         @ILT+65(ostream::operator<<) (00401046) ;00401046为00401046    jmp          CShape::CShape (004010a0)一看就知道是CShape的构造函数。
    00411555    mov          eax,dword ptr [ebp-4] ;把ecx给eax,即this,首地址为vtable的地址
    00411558    mov          dword ptr [eax+8],2   ;[eax+4]为b1,[eax+8]为b2,b2=0
    0041155F    mov          ecx,dword ptr [ebp-4] ;ecx=this
    00411562    mov         dword ptr [ecx],offset CRect1::`vftable' (00429114);把vftable的地址付给[ecx]这个内存,即[ecx]里面是aftable的地址,[ecx]=0x00429114
    跟踪:00429114里面有2个元素,一个0x00401050 ostream::operator,另外一个0x0040105a :CRect1::display(),初始化的时候,把虚函数的地址放进去。上面调用。

    00411568    mov          eax,dword ptr [ebp-4]
    0041156B    pop          edi
    0041156C    pop          esi
    0041156D    pop          ebx
    0041156E    add          esp,44h
    00411571    cmp          ebp,esp
    00411573    call         __chkesp (004011e0)
    00411578    mov          esp,ebp
    0041157A    pop          ebp
    0041157B    ret

        主要部分在蓝色部分, 2个地方,前一个是如何调用虚函数,后面一个在构造函数中如何初始化构造函数。

  • 相关阅读:
    C++中无数据成员的类的对象占用内存大小
    malloc在函数内分配内存问题
    字符数组拷贝与strcpy函数
    Python,anaconda及pycharm安装过程笔记
    DFS-深度优先搜索与BFS-广度优先搜索
    90 k数和 II
    88 Lowest Common Ancestor of a Binary Tree
    AdaBoost笔记之代码
    79 最长公共子串
    78 最长公共前缀
  • 原文地址:https://www.cnblogs.com/shakin/p/3714921.html
Copyright © 2011-2022 走看看