分享

C++虚函数的实现细节、虚析构函数 汇编解析

 astrotycoon 2015-10-25

   C++里多态的实现,依靠的是虚函数的运行时函数地址确定,不过真正的实现过程,还是在编译阶段。编译器究竟对虚函数做了怎样的处理?这就是本文所描述的。然后又对虚函数中最特殊的虚析构函数的运行情况进行了分析。
   1、类的存储空间
   在INTEL 32 CPU,VC6环境下,空类的一个实例占一个字节(特例);
   一个C++类本身(注意:不是对象),在内存里是有信息的, 比如虚函数表、静态成员变量。
   函数虚函数的类,它的每一个对象实例,在内存中,头四个字节存放的都是虚函数表指针。
   2、虚函数的实现过程
   对虚函数,编译器不给出直接的函数调用地址,而是关于一个未知量的表达式,这个参数就是虚函数表的指针。使用虚函数实现C++多态的方法,网上很多讲解,这里不多讲,本文结合源代码和部分汇编代码,分析了编译过程中对虚函数的处理。
   3、虚析构函数
   无论基类的析构函数是否为虚析构函数. 基类的析构函数总是会被自动调用的;但是, 如果用基类指针去操作一个了派生类对象,那么在delete这个基类指针时,派生类的析构函数将不会被调用。

   4. 本文为原创,转载或其他用途请注明出处:http://blog.csdn.net/ydbcsdn/archive/2008/10/24/3137384.aspx

  1. // VC测试代码
  2. #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers
  3. #include <stdio.h>
  4. class Base;
  5. class Derived;
  6. void GFunction(void);
  7. int main(int argc, char* argv[])
  8. {
  9.     GFunction();
  10.     char a = 127;
  11.     a+=1;
  12.     printf("new a = %d/n", a);
  13.     getchar();
  14.     return 0;
  15. }
  16. class Base
  17. {
  18. public:
  19.     Base::Base()
  20.     {
  21.     };
  22.     virtual Base::~Base()
  23.     {
  24.         printf("Base deconstruct/n");
  25.     };
  26.     virtual void Fun()
  27.     {
  28.     };
  29.     int a ;
  30. };
  31. class Derived : public Base 
  32. {
  33. public:
  34.     Derived::Derived()
  35.     {
  36.     };
  37.     virtual Derived::~Derived()
  38.     {
  39.         printf("Derived deconstruct/n");
  40.     };
  41.     virtual void Fun()
  42.     {
  43.     };
  44. };
  45. void GFunction(void)
  46. {
  47.     printf("Class Base    Sizeof =%d /n", sizeof(Base));
  48.     printf("Class Derived Sizeof =%d /n/n", sizeof(Derived));
  49.     Base* pA = (Base*)new Derived;
  50.     pA->Fun();  // 虚函数调用
  51.     delete pA;
  52. }
  pA->Fun()的汇编代码如下:
  1. 59:       pA->Fun();
  2. 0040D75C       mov         edx,dword ptr [ebp-10h]  // edx为pA
  3. 0040D75F       mov         eax,dword ptr [edx]      // eax为pA对象的虚表指针pVTable
  4. 0040D761       mov         esi,esp
  5. 0040D763       mov         ecx,dword ptr [ebp-10h]  // this指针存入ecx
  6. 0040D766       call        dword ptr [eax+4]        // 函数地址:虚表指针+4, 就是虚表中第二项
  7. 0040D769       cmp         esi,esp
  8. 0040D76B       call        __chkesp (00401b20)
   1. 如果成员函数不是虚函数,那么编译的时候,就直接指定了调用函数的入口;
   2. 如果是虚函数,那么编译时不直接指定函数入口,而是先在对象的内存空间里取一个值(这个值就是虚函数表的地址,放在对象内存空间的最前面4个字节里)。汇编代码中会有取值的过程;
   3. 虚函数表中按顺序存放着虚函数在地址空间中的地址:的第一个DWORD存储的就是第一个虚函数的地址,第二个DWORD存储的就是第二个虚函数的地址;
   3. 编译器在编译过程中已经知道你调的那个函数在虚函数表中的序号。汇编代码中会有体现;
   4. 在运行时,就能正确找到调用函数的地址,并调用它.

  
     析构函数的一点补充:
  1. 在一个项目中,如果有N层派生类,编译器总是保证所有基类的析构函数都被依次调用,但问题是,究竟从那层开始调用呢?对于非虚析构函数,显然是在编译期间就直接确定的,对虚析构函数,在运行时,才能确定是从哪一层开始往下层调用(基类)。 
  2.   事实上,和一般虚函数一样,运行时,才确定要调用的析构函数,不过有些不同的是,析构函数执行完后,下一条指令就是基类析构函数的CALL指令,一直到最上层为止。 
  3. 下面是一段debug下的反汇编,其中Base派生自BaseBase.可以看到~Base调用后,会自动调用~BaseBase 
  4.     virtual Base::~Base() // 基类析构函数 
  5.     { 
  6. 00401740  push        ebp  
  7. 00401741  mov        ebp,esp 
  8. 00401743  sub        esp,0CCh 
  9. 00401749  push        ebx  
  10. 0040174A  push        esi  
  11. 0040174B  push        edi  
  12. 0040174C  push        ecx  
  13. 0040174D  lea        edi,[ebp-0CCh] 
  14. 00401753  mov        ecx,33h 
  15. 00401758  mov        eax,0CCCCCCCCh 
  16. 0040175D  rep stos    dword ptr es:[edi] 
  17. 0040175F  pop        ecx  
  18. 00401760  mov        dword ptr [ebp-8],ecx 
  19. 00401763  mov        eax,dword ptr [this] 
  20. 00401766  mov        dword ptr [eax],offset Base::`vftable' (44533Ch) 
  21.         printf("Base deconstruct/n"); 
  22. 0040176C  push        offset string "Base deconstruct/n" (445344h) 
  23. 00401771  call        printf (405760h) 
  24. 00401776  add        esp,4 
  25.     }; 
  26. 00401779  mov        ecx,dword ptr [this] 
  27. 0040177C  call        BaseBase::~BaseBase (4017A0h)  // 上层基类也紧跟着调用了

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多