这几天被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/archive/2013/03/03/2939439.html
欢迎关注
微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍
原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/79443
非原创文章文中已经注明原地址,如有侵权,联系删除
关注公众号【高性能架构探索】,第一时间获取最新文章
转载文章受原作者版权保护。转载请注明原作者出处!