我们大家都知道 OC 类的本质是结构体, 那么如果我们想搞清楚 OC 对象的内存分配规则, 就必须先搞清楚结构体的内存分配规则.所以今天先来探索结构体的内存分配规则, 属性的类型可以是对象, 同样结构体的成员也可以是结构体, 就是结构体嵌套, 按照国际惯例我们先从简单的着手探索, 然后再探索复杂的情况.
设备情况:
- Mac 系统, Intel 64位
- IDE: Xcode
约定:
长度: 指类型被分配内存的大小, 或者 变量被分配内存的大小;
普通成员: 基本数据类型, 为能再分解为简单数据类型;
叶子成员: 成员或者嵌套结构体中结构体成员的成员, 直到不能再分解为其他数据类型,
分类: 按照复杂程度咱们自己暂时给分为两种情况:
基本结构体: 无嵌套, 成员都是基本数据类型嵌套结构体: 有成员是结构体类型的, 可以多级嵌套, 这里只探索二级嵌套的情况.
基本结构体:
- 定义
6个结构体,Struct4和Struct5这两个结构体成员相同, 但是成员声明的顺序不同.
typedef struct {
char c; // 1
} Struct0;
typedef struct {
short d; // 2
} Struct1;
typedef struct {
int b; // 4
} Struct2;
typedef struct {
char c; // 1
short d; // 2
int b; // 4
} Struct3;
typedef struct {
double a; // 8
int b; // 4
char c; // 1
short d; // 2
} Struct4;
typedef struct {
double a; // 8
char c; // 1
int b; // 4
short d; // 2
} Struct5;
- 然后打印他们的长度.
void test() {
printf("char 内存: %lu \n", sizeof(char));
printf("short 内存: %lu \n", sizeof(short));
printf("int 内存: %lu \n", sizeof(int));
printf("double 内存: %lu \n", sizeof(double));
printf("\n");
printf("Struct0 的内存: %lu \n", sizeof(Struct0));
printf("Struct1 的内存: %lu \n", sizeof(Struct1));
printf("Struct2 的内存: %lu \n", sizeof(Struct2));
printf("Struct3 的内存: %lu \n", sizeof(Struct3));
printf("Struct4 的内存: %lu \n", sizeof(Struct4));
printf("Struct5 的内存: %lu \n", sizeof(Struct5));
}
打印结果如下图, 记为图一:
- 他们长度分别是 1, 2, 4, 8, 16, 24;
Struct4和Struct5, 他们各成员的长度总和是1+2+4+8=15, 而他们的长度分别是16和24, 成员相同, 长度不同;Struct3各成员长度总和为7, 他的长度是8;
由此我们得出两个结论:
- 结构体长度
大于或者等于所有成员长度总和. - 两个结构体, 成员相同, 成员在结构体的先后顺序不同, 两个结构体长度也
有可能不同.
- 这个大家可以测试一下, 有相同的情况存在, 比如
short和char两个成员.
成员相同: 指同数据类型的成员数量相同
因此我们可以大胆猜测, 系统给结构体分配内存的时候, 肯定有一套规则, 按照这个规则去计算和分配内存. 既然跟顺序有关, 我们就按照顺序打印一下各成员的地址看看, 因为十六进制看着不太习惯, 所以同时输出了十进制数, 看起来更顺眼一些.
void test_2() {
Struct4 s4 = {1.0, 2, 'x', 3};
printf("s4: = %p -> %lu -> %lu \n", &s4, &s4, &s4+1);
printf(" a: = %p -> %lu -> double \n", &(s4.a), &(s4.a));
printf(" b: = %p -> %lu -> int \n", &(s4.b), &(s4.b));
printf(" c: = %p -> %lu -> char \n", &(s4.c), &(s4.c));
printf(" d: = %p -> %lu -> short \n", &(s4.d), &(s4.d));
printf("\n");
Struct5 s5 = {1.0, 'x', 2, 3};
printf("s5: = %p -> %lu -> %lu \n", &s5, &s5, &s5+1);
printf(" a: = %p -> %lu -> double \n", &(s5.a), &(s5.a));
printf(" c: = %p -> %lu -> char \n", &(s5.c), &(s5.c));
printf(" b: = %p -> %lu -> int \n", &(s5.b), &(s5.b));
printf(" d: = %p -> %lu -> short \n", &(s5.d), &(s5.d));
}
打印结果如下图, 记为图二:
我们根据打印结果来绘制 2 张表格, 来观察一下有什么规律. 地址位数太多, 前边我就给省略了. 方便观察
-
s4的内存结构
-
s5的内存结构
我们通过这两个表格和图一中打印结果的观察发现:
- 结构体的首地址就是第一个成员的地址;
- 所有成员的地址都是偶数, 但是并不是按照偶数地址去做内存分配的,
- 从
s5的成员b就可以看出, 并没有从编号22开始.
- 所有成员的地址相对于首地址的偏移量都是该成员长度的整数倍;
- 结构体被分配内存大小是最大成员长度的整数倍.
结论一:
系统给基本结构体分配内存时,每个成员的地址相对首地址的偏移量是这个成员长度的整数倍,总长度为最大长度成员的整数倍, 结构体的首地址就是第一个成员的地址;
嵌套结构体
定义三个结构体并打印各自长度 和 s8 的各成员长度, 到不能分解为止, s7 各成员自己打印验证即可.
// 总大小 16
typedef struct {
double e; // 8
int f; // 4
} Struct6;
// 总大小 24
typedef struct {
int a; // 4
Struct6 b; // 16
} Struct7;
void test_3() {
printf("Struct6 的内存: %lu \n", sizeof(Struct6));
printf("Struct7 的内存: %lu \n", sizeof(Struct7));
printf("\n");
Struct7 s7 = {1.0, {4,5,}};
printf("s7: = %p -> %lu -> %lu \n", &s7, &s7, &s7+1);
printf("\n");
printf(" a: = %p -> %lu -> int \n", &(s7.a), &(s7.a));
printf("\n");
printf(" b: = %p -> %lu -> %lu -> Struct6 \n", &(s7.b), &(s7.b), &(s7.b)+1);
printf("\n");
printf(" e: = %p -> %lu -> double \n", &(s7.b.e), &(s7.b.e));
printf(" f: = %p -> %lu -> int \n", &(s7.b.f), &(s7.b.f));
}
打印结果如下图, 记为图三:
这个比较简单, 所以就直接在截图上标出来了, 做表格太麻烦, 各位理解万岁吧😄.按照结论一给出的结果来看看是否符合.
Struct6的长度是16;Struct7的长度应该都是16的整数倍, 但是实际上不是;- 成员
b的首地址偏移量是8, 也不是16的整数倍, 而是8的位数,Struct7中成员长度最大是16, 但是结构体成员b的成员最大长度是8; - 结构体成员
b的大小仍然是16, 这和单独声明一个变量是相同的. - 总长度是
24, 也不是16的整数倍, 如果是按4的倍数,20就可以, 很显然不是,24明显是b中e长度的整数倍;
结论二:
系统给嵌套结构体分配内存时,普通成员的地址相对首地址的偏移量是这个成员长度的整数倍,结构体成员的首地址是其长度最长的成员的整数倍, 嵌套情况依次类推; 总长度为所有叶子成员中最大长度的整数倍, 结构体的首地址就是第一个成员的地址;
最终总结:
系统给结构体分配内存时的规则
- 结构体的成员,第一个成员地址相对于结构体首地址偏移量为
0, 即结构体首地址, - 从第二个成员开始, 在前一个成员空间地址之后(即>=),
普通成员首地址是该成员长度的整数倍;结构体成员首地址是他所有叶子成员长度中最大长度的整数倍. - 总长度是所有叶子成员长度最大长度的整数倍.
注意: sizeof是操作符, 而不是函数. 检测的是数据类型长度.
其实这些例子还不是很充足, 有些情况有可能没有展示出来, 大家可以多测试, 欢迎多交流, 共同进步.