【c++内存分布系列】单独一个类
首先要明确类型本身是没有具体地址的,它是为了给编译器生成相应对象提供依据。只有编译器生成的对象才有明确的地址。
一、空类
形如下面的类A,类里没有任何成员变量,类的sizeof值为1。
#include <cstdio>
class A { }; int main(int argc, char** argv) { printf("%d\n", sizeof(A));//类型A的大小 A a; A* pa = &a; printf("%08x\n", pa);//对象a的地址 printf("%02x\n", *pa);//对象a的内容 }
windows输出:
1
0023fc9b
cc
这是因为编译器在编译时为类A插入了一个char型变量,以便确定类A的对象在内存的位置。插入的变量不进行初始化,所以看到内存中的内容为cc
二、包含成员函数但没有成员变量
只包含成员函数没有成员变量的类形式如下:
#include <cstdio>
class A { public: void fun(){}; }; int main(int argc, char** argv) { printf("%d\n", sizeof(A));//类型A的大小 A a; A* pa = &a; printf("%08x\n", pa);//对象a的地址 printf("%02x\n", *pa);//对象a的内容 void (A::*p)(); p = &A::fun; printf("%08x\n", p);//成员方法fun的地址 }
windows的输出:
1
001aff23
cc
003211a4
因为成员函数有自己的地址,不随对象而变化,所以类型A的大小依然是1。查看内存003211a4有:
A::fun:
003211A4 E9 17 03 00 00 jmp A::fun (3214C0h)
直接跳转到3214c0h,再查看3214c0h有:
1: #include <cstdio>
2:
3: using namespace std;
4:
5: class A
6: {
7: public:
8: void fun(){};
003214C0 55 push ebp
003214C1 8B EC mov ebp,esp
003214C3 81 EC CC 00 00 00 sub esp,0CCh
即源码的汇编程序。到此应该可以理解为什么成员函数不需要随对象的生成而分配地址了。因为所有对象的方法对应的汇编执行都应该是一样的,所以存在一份足够了,不需要每个对象都重新分配该段代码的空间。
三、包含成员变量
类A包含成员变量形式如下:
#include <cstdio>
class A { public: int a; void fun(){}; }; int main(int argc, char** argv) { printf("%d\n", sizeof(A));//类型A的大小 A a; a.a = 0xaaaaaaaa;//给成员变量赋值 A* pa = &a; printf("%08x\n", pa);//对象a的地址 printf("%08x\n", *pa);//对象a的内容 void (A::*p)(); p = &A::fun; printf("%08x\n", p);//成员方法fun的地址 }
windows输出:
4
0019fc9c
aaaaaaaa
00b411a4
由于包含成员变量,类A生成的每个对象的成员变量值不一定相同,所以成员变量要随每个对象的生成分配自己的空间。此例的成员变量为int型,所以sizeof类A的值就为类A成员变量占用内存大小的总和,此例为4。如果有更多的成员变量,sizeof的值则为他们的和,内存也依次为他们的值排列,这里就不举例了。查看对象a的内容为0xaaaaaaaa,也验证了该地址存放的是成员变量的值。
四、包含静态方法和静态成员变量
#include <cstdio>
class A { public: int a; void fun(){}; static int sa; static void sfun(){}; }; int A::sa = 0xbbbbbbbb;//给静态成员变量赋值 int main(int argc, char** argv) { printf("%d\n", sizeof(A));//类型A的大小 A a; a.a = 0xaaaaaaaa;//给成员变量赋值 A* pa = &a; printf("%08x\n", pa);//对象a的地址 printf("%08x\n", *pa);//对象a的内容 void (A::*p)(); p = &A::fun; printf("%08x\n", p);//成员方法fun的地址 printf("%08x\n", &(A::sa));//静态成员变量的地址 printf("%08x\n", A::sa);//静态成员变量的值 printf("%08x\n", A::sfun);//静态方法的地址 }
windows输出:
4
001ffaa0
aaaaaaaa
012011a4
01207000
bbbbbbbb
012011d1
很简单,大家都知道,静态方法和静态变量都有自己的地址,当然也就不会随对象的生成而重新分配,所以sizeof(A)的值依然是4。
去内存0x01207000查看静态成员地址的内容为0xbbbbbbbb,ok没问题,再来看看静态方法内存对应的内容:
10: static int sa;
11: static void sfun(){};
01201580 55 push ebp
01201581 8B EC mov ebp,esp
01201583 81 EC C0 00 00 00 sub esp,0C0h
没错,代码对应的汇编。这下就清晰了。
五、包含虚函数
加入两个虚函数,其他代码不变。
#include <cstdio>
class A { public: int a; void fun(){}; static int sa; static void sfun(){}; virtual void vfun1(){}; virtual void vfun2(){}; }; int A::sa = 0xbbbbbbbb;//给静态成员变量赋值 int main(int argc, char** argv) { printf("%d\n", sizeof(A));//类型A的大小 A a; a.a = 0xaaaaaaaa;//给成员变量赋值 A* pa = &a; printf("%08x\n", pa);//对象a的地址 printf("%08x\n", *pa);//对象a的内容 void (A::*p)(); p = &A::fun; printf("%08x\n", p);//成员方法fun的地址 printf("%08x\n", &(A::sa));//静态成员变量的地址 printf("%08x\n", A::sa);//静态成员变量的值 printf("%08x\n", A::sfun);//静态方法的地址 }
windows下输出:
8
0021fa7c
0125574c
012511d6
01257000
bbbbbbbb
01251208
之所以加入两个虚函数,是为了验证编译器只生成了一个虚表,续表的概念不明白的话要查下。也正是因为加入了虚表,内存增加了4,即sizeof(A)的结果变成了8。查看对象a的内存:
0021FA7C 4C
0021FA7D 57
0021FA7E 25 01 AA AA AA
0021FA83 AA
除了0xaaaaaaaa还新增了0x0125574c,这就是虚表的地址。查看该地址内容:
A::`vftable':
0125574C 27
0125574D 11 25 01 86 11 25
01255753 01
包含了两个地址,分别为:0x01251186,0x01251127。这两个地址就是对应两个虚函数,下面只拿其中一个来分析,另一个一样。查看0x01251186:
A::vfun2:
01251186 E9 25 05 00 00 jmp A::vfun2 (12516B0h)
此处直接跳转到vfun2的执行处,内容如下:
13: virtual void vfun2(){};
012516B0 55 push ebp
012516B1 8B EC mov ebp,esp
012516B3 81 EC CC 00 00 00 sub esp,0CCh
012516B9 53 push ebx
没错,vfun2的汇编。
至此一个类的内存所有情况都分析到了,如果该类中包含其他类的对象,那么先计算那个对象的内存占用大小,然后把它当做普通的成员变量看待就可以了。