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
【推荐】还在用 ECharts 开发大屏?试试这款永久免费的开源 BI 工具!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步