zoukankan      html  css  js  c++  java
  • c++对象内存布局与c++成员函数指针

    这几天被c++成员函数指针的问题搞得晕头转向

    下面来慢慢整理下c++对象内存布局与c++成员函数指针的知识

    c++对象内存布局

    1 成员函数如何实现的?跟普通函数了有什么区别?

     成员函数需要传递this指针,以普通的成员函数为例:

    obj* oo1=new obj;
        oo1->foo();

    00FF9916 mov ecx,dword ptr [ebp-80h]  //传递对象地址到ecx
    00FF9919 call obj::foo (0FF6108h)  //调用函数

    foo()

    00FF9AFF pop ecx           //
    00FF9B00 mov dword ptr [ebp-8],ecx //写this到栈变量

    m_a=1;
    00FF9B03 mov eax,dword ptr [this] //this mov 到eax[this]不知道具体使用ecx还是用[ebp-8]? 
    00FF9B06 mov dword ptr [eax+4],1  //寻址成员变量

    普通函数只有一个call指令,不回去传this

    2 单继承的内存结构

    单继承非常简单,就像叠罗汉一样,一个个叠上去好了

    struct CA
    {
        int a;
    };
    struct CB:public CA
    {
        int b;
    };
    struct test:public CB
    {
        int c;
    };
    1>  class test    size(12):
    1>      +---
    1>      | +--- (base class CB)
    1>      | | +--- (base class CA)
    1>   0    | | | a
    1>      | | +---
    1>   4    | | b
    1>      | +---
    1>   8    | c
    1>      +---
    1>  

    先来后到,基类在低地址,子类在高地址,一片和谐  这样它们的指针也非常好处理,都指向基地址就OK了

    3 单继承的虚函数怎么实现的?

    虚函数的主要特性就是覆盖,子类覆盖父类,基本原理是在类中加一个虚表指针,指向该类的虚函数表,虚函数表中存储了各个函数的地址,子类只要重写父类的虚函数表就行了

    struct CA
    {
        int a;
        virtual void foo1(){}
        virtual void foo2(){}
    };
    struct CB:public CA
    {
        virtual void foo1(){}
        int b;
    };
    struct test:public CB
    {
        virtual void foo2(){}
        int c;
    };

    1> class test size(16):
    1> +---
    1> | +--- (base class CB)
    1> | | +--- (base class CA)
    1> 0 | | | {vfptr}         //这三个类共用用一个虚表指针 指向三个不同的虚表
    1> 4 | | | a
    1> | | +---
    1> 8 | | b
    1> | +---
    1> 12 | c
    1> +---
    1>
    1> test::$vftable@:      //虚表内容
    1> | &test_meta
    1> | 0
    1> 0 | &CB::foo1        //0号位 被CB覆盖
    1> 1 | &test::foo2      //1号位背test覆盖

     

    调用细节:

    test* ptest=new test;
        ptest->foo2();
    0022993C  mov         eax,dword ptr [ebp-80h]  //eax=ptest
    0022993F  mov         edx,dword ptr [eax]     //edx=vfptr(虚表指针在类的首部),当然 不同编译器实现可能不一样
    00229941  mov         esi,esp            
    00229943  mov         ecx,dword ptr [ebp-80h]  //ecs=this
    00229946  mov         eax,dword ptr [edx+4]    //eax=&test::foo2 (函数指针)
    00229949  call        eax              

    可以看出虚函数的调用要比普通成员函数多寻址2次,一次找虚表地址,一次找函数地址

    可以看出虚表指针大小为sizeof(int)

    3 多继承的内存结构(不考虑钻石继承)

    多继承!这下麻烦来了...

    struct CA
    {
        int a;
    
    };
    struct CB
    {
        int b;
    };
    struct CC
    {
        int c;
    };
    struct test:public CA,public CB,public CC
    {
    
        int te;
    };
    1>  class test    size(16):
    1>      +---
    1>      | +--- (base class CA)
    1>   0    | | a
    1>      | +---
    1>      | +--- (base class CB)
    1>   4    | | b
    1>      | +---
    1>      | +--- (base class CC)
    1>   8    | | c
    1>      | +---
    1>  12    | te
    1>      +---

    现在不再是1个基类而是n个基类了,咋办? 没办法 挨个排呗

    这样导致的后果是子类指针转换成基类指针时指针的值会变化,但是这并不影响我们比较基类和子类的指针时候指向同一个对象

    test* ptest=new test;
        CA* d1=ptest;
        CB* d2=ptest;
        CC* d3=ptest;
        bool b11=ptest==d1;
    ptest: xxx08
    d1:     xxx08    //依次增长
    d2:     xxx0c    //
    d3:     xxx10    //
    
    bool b11=ptest==d1;                //编译器知道他们的地址相同,直接比较
    003F9933  mov         eax,dword ptr [ebp-80h]  
    003F9936  xor         ecx,ecx  
    003F9938  cmp         eax,dword ptr [ebp-8Ch]  
    003F993E  sete        cl  
    003F9941  mov         byte ptr [ebp-0ADh],cl  
    bool b22=ptest==d2;                //它们之间差几个地址,
    012E98B7 cmp dword ptr [ebp-80h],0 
    012E98BB je memcall+0DBh (12E98CBh) 
    012E98BD mov eax,dword ptr [ebp-80h] 
    012E98C0 add eax,4                 //编辑器补齐差值 然后比较
    012E98C3 mov dword ptr [ebp-228h],eax 
    012E98C9 jmp memcall+0E5h (12E98D5h) 
    012E98CB mov dword ptr [ebp-228h],0 
    012E98D5 mov ecx,dword ptr [ebp-228h] 
    012E98DB xor edx,edx 
    012E98DD cmp ecx,dword ptr [ebp-98h] 
    012E98E3 sete dl 
    012E98E6 mov byte ptr [ebp-0B9h],dl

    4 多继承中的虚函数

    多继承虚函数的关键是使用多个虚表

    一个虚表是够用的,基类们没法共用一个虚表(只要考虑到这些类还会被用在其它的继承树中,就可以明白这一点)

    struct CA
    {
        int a;
        virtual void fooA(){}
    };
    struct CB
    {
        int b;
        virtual void fooB(){}
    };
    struct CC
    {
        int c;
        virtual void fooC(){}
    };
    struct test:public CA,public CB,public CC
    {
        virtual void fooT(){te=0xab;}
        int te;
    };
    1>  class test    size(28):
    1>      +---
    1>      | +--- (base class CA)
    1>   0    | | {vfptr}
    1>   4    | | a
    1>      | +---
    1>      | +--- (base class CB)
    1>   8    | | {vfptr}
    1>  12    | | b
    1>      | +---
    1>      | +--- (base class CC)
    1>  16    | | {vfptr}
    1>  20    | | c
    1>      | +---
    1>  24    | te
    1>      +---

    再看看虚表的结构:

    1>  test::$vftable@CA@:
    1>      | &test_meta
    1>      |  0          //虚表偏移
    1>   0    | &CA::fooA
    1>   1    | &test::fooT
    1>  
    1>  test::$vftable@CB@:
    1>      | -8          //虚表偏移
    1>   0    | &CB::fooB
    1>  
    1>  test::$vftable@CC@:
    1>      | -16        //虚表偏移
    1>   0    | &CC::fooC

    对于类test,编译器生成了三个虚表(注意虚表的符号表明他们是属于test的)! 现在再加上CA CB CC的虚表,这四个类一共使用了6个虚表

    我以前以为多继承下子类也会去使用基类的虚表,实际上不是的

    看代码:

         test* ptest=new test;
        CB* d2=ptest;
        ptest->fooB();
        d2->fooB();
    
    
        ptest->fooB();
    0104C416  mov         ecx,dword ptr [ebp-80h]  
    0104C419  add         ecx,8                  //转换为CB的地址,不进行转换仍然可以调到子类的函数,但是没法调用父类的函数了,必须进行转换
    0104C41C  mov         eax,dword ptr [ebp-80h]  
    0104C41F  mov         edx,dword ptr [eax+8]        //找到虚表地址
    0104C422  mov         esi,esp  
    0104C424  mov         eax,dword ptr [edx]  
    0104C426  call        eax  
    0104C428  cmp         esi,esp  
    0104C42A  call        @ILT+4860(__RTC_CheckEsp) (1026301h)  
        d2->fooB();
    0104C42F  mov         eax,dword ptr [ebp-8Ch]      
    0104C435  mov         edx,dword ptr [eax]          
    0104C437  mov         esi,esp  
    0104C439  mov         ecx,dword ptr [ebp-8Ch]  
    0104C43F  mov         eax,dword ptr [edx]  
    0104C441  call        eax  
    0104C443  cmp         esi,esp  
    0104C445  call        @ILT+4860(__RTC_CheckEsp) (1026301h)  
    
    virtual void fooB(){te=0xab;}
    ....
    01029F0F  pop         ecx  
    01029F10  mov         dword ptr [ebp-8],ecx  
    01029F13  mov         eax,dword ptr [this]  
    ....
    01029F16  mov         dword ptr [eax+10h],0ABh      //这个偏移量是以基类CB为基准的

    现在对象如何找到对应的虚表呢? 加上一个偏移值就好了,这是编译期完成的

    函数如何得到正确的偏移量?这也是编译期完成的

    5 虚继承怎么办?

    首先看看没有不使用虚继承的钻石继承 

    struct CA
    {
        int a;
    };
    struct CB:public CA
    {
        int b;
    };
    struct CC:public CA
    {
        int c;
    };
    struct test:public CB,public CC
    {
        int te;
    };
    1>  class test    size(20):
    1>      +---
    1>      | +--- (base class CB)
    1>      | | +--- (base class CA)
    1>   0    | | | a
    1>      | | +---
    1>   4    | | b
    1>      | +---
    1>      | +--- (base class CC)
    1>      | | +--- (base class CA)
    1>   8    | | | a
    1>      | | +---
    1>  12    | | c
    1>      | +---
    1>  16    | te
    1>      +---

    可以看出CA有两份,这样会引起很多问题,我们考虑把重复的基类放在单独的结构中,因此有了虚继承:

    struct CA
    {
        int a;
    };
    struct CB:virtual public CA
    {
        int b;
    };
    struct CC:virtual public CA
    {
        int c;
    };
    struct test:public CB,public CC
    {
        int te;
    };
    1>  class test    size(24):      //少了个CA但是增加了两个vbptr(虚基类表指针)
    1>      +---
    1>      | +--- (base class CB)
    1>   0    | | {vbptr}
    1>   4    | | b
    1>      | +---
    1>      | +--- (base class CC)
    1>   8    | | {vbptr}
    1>  12    | | c
    1>      | +---
    1>  16    | te
    1>      +---
    1>      +--- (virtual base CA)
    1>  20    | a
    1>      +---
    1>  
    1>  test::$vbtable@CB@:
    1>   0    | 0
    1>   1    | 20 (testd(CB+0)CA)
    1>  
    1>  test::$vbtable@CC@:
    1>   0    | 0
    1>   1    | 12 (testd(CC+0)CA)
    1>  
    1>  
    1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
    1>                CA      20       0       4 0

    我们现在又多了个表! 虚基类表的作用是记录虚基类的偏移  

    看看test如何使用CA的成员:

    test* ptest=new test();
    
        ptest->a=1;
    
    009F991E  mov         eax,dword ptr [ebp-80h]  
    009F9921  mov         ecx,dword ptr [eax]      //ecx=vbptr
    009F9923  mov         edx,dword ptr [ecx+4]     //edx=offset
    009F9926  mov         eax,dword ptr [ebp-80h]    //eax=ptest
    009F9929  mov         dword ptr [eax+edx],1    //eax+edx即为a的地址

    再加上虚函数

    struct CA
    {
        int a;
        virtual void fooA(){}
    };
     
    struct CB:virtual public CA
    {
        int b;
        virtual void fooB(){}
    };
    struct CC:virtual public CA
    {
        int c;
        virtual void fooC(){}
    };
    struct test:public CB,public CC
    {
        virtual void fooT(){}
        int te;
    };
    
    1>  class test    size(36):
    1>      +---
    1>      | +--- (base class CB)
    1>   0    | | {vfptr}
    1>   4    | | {vbptr}
    1>   8    | | b
    1>      | +---
    1>      | +--- (base class CC)
    1>  12    | | {vfptr}
    1>  16    | | {vbptr}
    1>  20    | | c
    1>      | +---
    1>  24    | te
    1>      +---
    1>      +--- (virtual base CA)
    1>  28    | {vfptr}
    1>  32    | a
    1>      +---
    1>  
    1>  test::$vftable@CB@:
    1>      | &test_meta
    1>      |  0
    1>   0    | &CB::fooB
    1>   1    | &test::fooT
    1>  
    1>  test::$vftable@CC@:
    1>      | -12
    1>   0    | &CC::fooC
    1>  
    1>  test::$vbtable@CB@:
    1>   0    | -4          //vbptr相对于CB的偏移
    1>   1    | 24 (testd(CB+4)CA)
    1>  
    1>  test::$vbtable@CC@:
    1>   0    | -4          //vbptr相对于CC的偏移
    1>   1    | 12 (testd(CC+4)CA)
    1>  
    1>  test::$vftable@CA@:
    1>      | -28          //相对于根地址
    1>   0    | &CA::fooA
    1>  
    1>  test::fooT this adjustor: 0
    1>  
    1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
    1>                CA      28       4       4 0

    不过vbi里面的vbptr vbyte vVtorDisp又是什么呢?。。

    这下子一切都和谐了,下面看看成员函数指针

    成员函数指针

    1,成员函数指针的声明方式

    恩。。熟悉c函数指针声明的同学一定知道普通函数指针的声明方式:

    typedef  void (*func1)();   //声明函数指针

    func1   f1; //定义函数指针    

    成员函数指针也有类似的声明规则:

    typedef void (C::*func1)();   //C是类名

    2,成员函数指针怎么用?

      类C可以调用类C的成员函数指针,例如:

    typedef void (test::*func_t)();
    func_t ft=&test::fooc;
    test* pt=new test();
    (pt->*ft)();

      类C也可以调用基类的成员函数指针,编译器会转换this指针到实际的地址然后传给成员函数

      基类指针调用基类的虚函数指针,会有多态的效果吗?

      答:不会!  函数指针并不关心它本身是不是虚函数(由声明也可以看出来),它指向的就是基类的函数地址,所以这样是不会有多态效果的

        虚表能实现多态是因为虚函数的调用会先去查询虚表,但是使用成员函数指针不会去查虚表,因为函数地址已经在成员函数指针里面了

    答:会有多态效果,参见http://www.cnblogs.com/mightofcode/archive/2013/03/31/2991823.html

    3,成员函数指针等于函数指针么?

      或者说成员函数指针只保存了函数地址么?

      不是这样的,比如在MSVC中,成员函数就可能是4,8,12字节

      也就是说成员函数保存的不只是函数地址,还保存了其它东西!

      我们一步步看看成员函数指针保存了哪些

      多继承情况下:

      

    struct CB//:virtual public CA
    {
        void foob(){}
        int b;
    };
    struct CC//:virtual public CA
    {
        void fooc(){}
        int c;
    };
    
    struct test:public CB,public CC 
    {
        void foot(){}
    };
    
    typedef void (test::*func_t)();
    int n=sizeof(func_t)    //8 func_t ft
    =&test::fooc;             //因为test继承自CC,func_t可以指向test::fooc 也就是CC::fooc        unsigned int l1=((unsigned long*)ppp1)[0];  //函数地址 unsigned int l2=((unsigned long*)ppp1)[1];  //基类的偏移量 4 如果ft指向的是test的成员函数,这个值就是0
    func_t现在指向CC::fooc 但是CC::fooc是接受CC*的,因此需要根据test*和偏移量计算出CC*的值
    func_t不知道自己指向的是哪个函数,所以它必须包含一个偏移量来修改this
    所以func_t现在是8个字节

    虚继承情况下:
    struct CD
    {
        int d;
    };
    struct CF
    {
        int f;
        void food(){}
    };
    
     struct CA
    {
        virtual void fooa1(){}
        void fooa2(){}
        int a;
    };
    struct CB:virtual public CA
    {
        void foob(){}
        int b;
    };
    struct test:public CB//,public CC 
    {
        void foot(){}
    };
    
    func_t ft=&test::food;
        n=sizeof(ft);                //12
    unsigned int l1=((unsigned long*)ppp1)[0];  //函数地址
    unsigned int l2=((unsigned long*)ppp1)[1];  //8 虚继承表偏移
    unsigned int l3=((unsigned long*)ppp1)[2];  //4 实际类在虚继承类中的偏移

    虚继承的情况下需要再存储虚继承记录在虚继承表中的偏移 ,而且也需要存储实际类在虚继承类中的偏移,这样总共有12个字节了

    不过网上有文章说"未知成员函数指针"有20字节,但是什么是"未知成员函数指针"呢?

    参考文章:http://blog.csdn.net/hifrog/article/details/33352

      

  • 相关阅读:
    领域驱动设计学习笔记
    Entity Framework 入门
    2019秋招复习笔记--面试经历记录总结
    Windows10 系统更新之后找不到输入法
    jumper-server-部署官网版
    docker java环境 直接做成镜像 跑自己的java包
    联想thinkpad如何关闭触摸板
    docker 安装
    阿里yum源与华为yum源的配置
    Backbone简介
  • 原文地址:https://www.cnblogs.com/mightofcode/p/2939439.html
Copyright © 2011-2022 走看看