类对象的存储结构
首先看下一个空的对象占用的内存大小
1 2 3 4 5 6
| class Student{}; int main(){ Student stu; printf("stu size:%lu\n",sizeof(stu)); }
|
下面是结果,说明一个对象的内存占用至少是1个字节
然后再看看增加成员变量的结构体的内存占用
1 2 3 4 5 6 7 8
| class Student{ private: int arg; }; int main(){ Student stu; printf("stu size:%lu\n",sizeof(stu)); }
|
结果是int的大小
如果再增加一个char变量,结果就有区别了
1 2 3 4 5 6 7 8 9
| class Student{ private: int arg; char sex; }; int main(){ Student stu; printf("stu size:%lu\n",sizeof(stu)); }
|
这时的结果是8,因为底层为了高效的内存操作,有一个内存对齐的处理,如果不足4的倍数,则补全,比如此时4字节+1字节,则对齐到8字节。
类的成员除了成员变量,还有成员函数,那么成员函数占用多少内存呢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class Student{ private: int arg; char sex; public: void show() { printf("show()\n"); } }; int main(){ Student stu; stu.show(); printf("stu size:%lu\n",sizeof(stu)); }
|
结果依然为8,这里说明了成员函数并不是存储在类对象的内存结构中。而我们虽然在函数中能够访问到类对象的成员变量,实际上是因为当类对象调用成员函数时,默认将类对象作为第一个参数传递
1 2
| @"show()\r\n" @"stu size:8\r\n"
|
然后用ida解析看看这个show经过编译器优化后的样子
1 2 3 4
| int __fastcall Student::show(Student *this) { return printf("show()\n"); }
|
静态成员函数和成员函数一样不占用空间
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Student{ private: int arg; char sex; public: void show(){ printf("show()\n"); } static void show2(){ printf("show2()\n"); } }; int main(){ Student stu; stu.show(); stu.show2(); printf("stu size:%lu\n",sizeof(stu)); }
|
结果同样是8个字节
1 2 3
| @"show()\r\n" @"show2()\r\n" @"stu size:8\r\n"
|
以上是简单的类对象内存布局,还有一种情况比较特殊情况,就是虚函数,现在看一个简单的虚函数例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| class Student{ private: int arg; char sex; public: virtual void virtual_show(){ printf("virtual_show()\n"); } }; int main(){ Student stu; stu.virtual_show(); printf("stu size:%lu\n",sizeof(stu)); }
|
这个例子的结果在32位中是12字节,在64位中是16字节。我现在是在64位中测试,所以结果是16字节,这里是怎么组合的呢,如果类中有虚函数,则会在内存的头部存放一个虚函数表的指针。而在32位中指针的大小是4字节,在64位中指针的大小是8字节,所以32位中4+1+4然后再内存对齐,结果就12字节,在64位中4+1+8再内存对齐,结果就是16字节
1 2
| @"virtual_show()\r\n" @"stu size:16\r\n"
|
那么如果有多个虚函数呢
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| class Student{ private: int arg; char sex; public: virtual void virtual_show(){ printf("virtual_show()\n"); } virtual void virtual_show2(){ printf("virtual_show2()\n"); } }; int main(){ Student stu; stu.virtual_show(); stu.virtual_show2(); printf("stu size:%lu\n",sizeof(stu)); }
|
结果依然是16字节,因为内存头部存储的是虚函数表的指针,而虚函数全部存储在虚函数表中
1 2 3
| @"virtual_show()\r\n" @"virtual_show2()\r\n" @"stu size:16\r\n"
|
刚刚说了,内存头部存储了虚函数指针,那么我们证实一下吧。看看这个对象16个字节的数据是装了些什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Student{ public: int arg; char sex; public: virtual void virtual_show(){ printf("virtual_show()\n"); } virtual void virtual_show2(){ printf("virtual_show2()\n"); } }; int main(){ Student stu; stu.arg=0x20; stu.sex='A'; stu.virtual_show(); stu.virtual_show2(); printf("stu size:%lu\n",sizeof(stu)); char* stu_pointer=(char*)&stu; for(int i=0;i<sizeof(stu);i++){ printf("%x ",stu_pointer[i]); } }
|
根据我们的分析,这个stu对象前面8个字节为虚函数表指针,然后4个字节为arg的数据,然后1个字节为sex的数据,最后3个字节为内存对齐的无用数据。那么这样理解下面的数据后就是20 10 0 0 1 0 0 0 是虚函数表指针,然后内存中存放的是小端序,转换一下就是0x1000102为虚函数表指针,20 0 0 0 为年龄,同样转换一下就是0x20。最后0x41是ascii码表中的’A’,而7f 0 0则是用来对齐的。
1 2 3 4
| @"virtual_show()\r\n" @"virtual_show2()\r\n" @"stu size:16\r\n" @"20 10 0 0 1 0 0 0 20 0 0 0 41 7f 0 0 "
|
头部的指针既然说他是虚函数表指针。那么是不是可以访问到虚函数呢。下面调用试一下,通过这种对象指针的方式访问虚函数表的函数,是可以访问到类对象的私有函数的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| class Student{ public: int arg; char sex; private: virtual void virtual_show(){ printf("virtual_show()\n"); } virtual void virtual_show2(){ printf("virtual_show2()\n"); } }; typedef void (*FUNC)(); int main(){ Student stu; stu.arg=0x20; stu.sex='A'; printf("stu size:%lu\n",sizeof(stu)); long stu_pointer=*(long*)&stu; FUNC show1=(FUNC)*(long*)stu_pointer; FUNC show2=(FUNC)*(long*)(stu_pointer+sizeof(int*)); show1(); show2(); }
|
最后成功的调用了这两个函数,我们主要缕一下怎么找到的这两个函数
1 2 3
| @"stu size:16\r\n" @"virtual_show()\r\n" @"virtual_show2()\r\n"
|
首先我们前面知道的,如果类中有虚函数,则会在类对象的内存最开始的位置存放一个虚函数表的指针。由此,我们首先拿到这个虚函数表的指针
long stu_pointer=*(long*)&stu;
上面这句中&stu这里会获取到stu的内存最开始的地址。也就是相当于。&stu这里的指针指向了虚函数表指针。stu_pointer最后这里得到的也就是虚函数表指针
FUNC show1=(FUNC)*(long*)stu_pointer;
虚函数表指针直接再获取一下指针的数据。则取到了虚函数的指针。
FUNC show2=(FUNC)*(long*)(stu_pointer+sizeof(int*));
如果是想要获取下一个虚函数的指针,则偏移一个指针的位置。这里也可以直接先转long*了之后再+1。同样可以偏移到下一个虚函数指针位置
FUNC show2=(FUNC)*((long*)(stu_pointer)+1);
这样同样可以偏移到第二个虚函数指针的位置
如果类再加上继承,多继承,虚函数。内存分布则会更加复杂。下一篇我再详细测试下比较复杂关系的类的布局情况。