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

  

原文地址:https://www.cnblogs.com/mightofcode/p/2939439.html