sizeof内存对齐和虚指针内存布局

来源:互联网 发布:淘宝俪兰化妆品怎么样 编辑:程序博客网 时间:2024/05/16 12:16
对于对象的内存布局与内存对齐这个问题,其实是没有统一的说法的,因为实际情况还要联系你的平台与编译器以及编译器的设置。我的是VC7XP系统,编译器的结构成员对齐设置为默认。下面针对两种情况来说说:

       在没有虚拟函数的情况下。对象的内存布局比较简单。看下面的类:

       class       A{

              public:

int m_a;

                     int m_b;

        private:

                     int m_c;

              public:

                     int GetValue(){ return m_c; }

       };

       实例化一个A的对象a,通过sizeof(a)我们知道这个对象的大小是12字节(3*4)。那么在这个12字节大小的内存空间,对象是怎样布局它的成员变量的呢?很简单,就是按成员变量的定义顺序来安排成员变量在对象对象内存中的位置。使用(int*&a可以得到对象a的地址,这个地址就是对象内存空间的首地址,嗯,应该很快就能想到这个地址也就是成员变量m_a的地址吧。怎样得到m_b的地址呢?(int*)&a+1就行了。同理(int*)&a+2就是m_c的地址了。看到这里你会突然想到什么呢?没错,我们居然可以访问到对象的私有成员。使用*((int*)&a+2) = 4可以为m_c赋值。虽然这很不符合规矩且破坏了面向对象中的封装,不过这可以帮助我们理解对象的内存布局,从这里也可以感受到C++强大的灵活性。

       可能你也很想知道成员函数GetValue的地址是放在哪里的。成员函数的地址并不是在对象的内存空间的,它和其它类外的函数一样,编译器会安排它们到某个内存空间。同样,使用一些“手段”你也可以获取成员函数的地址。由于我对这个也不是很了解,特别是怎么获取私有非虚成员函数的地址,所以这里就跳过不说了。

       下面再说说内存对齐,看下面的类:

       class       B{

              public:

int m_a;

                     short int m_b;

                     double m_c

        private:

                     int m_d;

              public:

                     int GetValue(){ return m_c; }

       };

       大家可以猜一下类B对象的大小。如果你猜是184+2+8+4),也不能说你错。但是你用sizeof(B)一看,结果竟然是24!多出来的6个字节是怎么回事呢?其实是内存对齐搞的鬼。在编译器的结构成员对齐设置为默认的情况下,分配给各个成员变量的内存大小似乎是向占最大空间的成员变量对齐的(这里我不敢肯定,还没看到权威的说法)。在B类中,首先为m_a分配空间,编译器一次为m_a分配8个字节(与最大成员m_c对齐),实际上m_a只占4个字节,还有4个字节多。接着编译器为m_b分配空间,经检查,m_b只占2字节,刚好前面还有4个字节多,所以m_b就放在前面多出的那4个字节空间中,现在已经为m_am_b分配了空间,但是m_a加上m_b也就只有6字节,还有2个字节多。如果下面分配的变量刚好是2字节的话,那就刚刚好装满8个字节,没有浪费空间,可是下面是要为double类型的变量分配空间,m_c占了8个字节,显然2个字节是装不下的,因此编译器再为m_c分配了8个字节的空间,刚装满。接下来又为m_d分配空间,根据之前的规则,编译器分配给m_d的空间也是8字节。这样看来,编译器总共为B的对象分配了8+8+8=24字节的空间。可能你觉得编译器这样做是浪费内存空间,但实际上这样做是很适合CPU做一些指令操作的,具体是怎样我不知道,一句话:用空间效率来换时间效率。如果你还是觉得空间比较重要,那么你可以通过设置编译属性或使用编译器指令#pragma来指定编译器所做的对齐方式,例如语句:#pragma pack(1)就是设置向1字节对齐。这时使用sizeof(B)得出的结果就是18了。

       现在来看有虚拟函数的情况,看下面的类:

       class       C{

              public:

int m_a;

                     int m_b;

        private:

                     int m_c;

              private:

                     virtual int GetValue(){ cout<<”I got it”<<endl;  }

       };

       在编译器的结构成员对齐设置为默认的情况下,sizeof(C)的值是16;因为编译器为我们多产生了一个指向虚函数表(VTABLE)的指针(__vfptr)。嗯,这个很容易理解。现在有一个任务,就是想办法调用类C的私有函数GetValue()

       为了调用这个私有函数,我们要想办法得到它的入口地址。于是我们会想到先定义一个函数指针,它将会用来保存这个入口地址,例如

typedef int (*FUNC)();

FUNC pFunc;//产生一个函数指针。

接下来就是把GetValue的入口地址给找出来。我们想到虚函数是放在VTABLE中,那么我们就要想办法得到指向VTABLE的指针__vfptr。还好,__vfptr就在对象内存中首地址,它比m_a还要前,先实例化C的一个对象objc,我们使用(int*&objc就可以得到一个指向__vfptr的指针了。然后(int*)*(int*)&objc就得到了__vfptr,接着我们就可以使用__vfptr来访问VTABLE中所保存的函数入口地址了。指针再深入(int*)*(int*)*(int*)&objc,这时候我们就得到了VTABLE中第一格(其实还有第0格,用来保存类型信息的)的函数地址,也就是GetValue()的入口地址。终于入了虎穴也得到了虎子,是该走人的时候啦,嗯,还得找个东西来装虎子吧,哈哈,笼子就是前面定义的那个函数指针pFuncpFunc=(FUNC) (int*)*(int*)*(int*)&objcOK大功告成!接着调用一下pFunc(),看看会输出什么?I got it!

最后,讲讲在有__vfptr的情况下的内存布局。在编译器的结构成员对齐设置为默认的情况下,__vfptr似乎很霸道,它所占有的空间不能和下一个成员变量共享。前面说过,当为一个成员变量分配空间的时候,编译器会检查分配给之前那个成员变量的空间是否还有剩,如果有且可以容纳的下当前的成员变量,那么编译器就会把当前成员变量装进之前多出来的空间中。但是编译器对__vfptr所处在的空间处理就不一样。如果最大成员变量的大小为8,那么编译器首先为__vfptr分配8个字节空间,还有4个多出来。但是这多出来的4个字节不会和下一个成员变量分享,无论下一个成员变量是多大,编译器都是重新为它分配空间。嗯,__vfptr在默认的情况下真好享受,一个人霸占了整个“房间”。不过一使用#pragma pack(1)__vfptr就没有这种特权了。

终于写完了,累,不过收获很大,谢谢观赏,有错请指出。^_^

原创粉丝点击