类对象的存储结构

首先看下一个空的对象占用的内存大小

1
2
3
4
5
6
class Student{};
int main(){
Student stu;
printf("stu size:%lu\n",sizeof(stu));
}

下面是结果,说明一个对象的内存占用至少是1个字节

1
@"stu size:1\r\n"

然后再看看增加成员变量的结构体的内存占用

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的大小

1
@"stu size:4\r\n"

如果再增加一个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
@"stu size:8\r\n"

类的成员除了成员变量,还有成员函数,那么成员函数占用多少内存呢

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);

这样同样可以偏移到第二个虚函数指针的位置

如果类再加上继承,多继承,虚函数。内存分布则会更加复杂。下一篇我再详细测试下比较复杂关系的类的布局情况。