iOS 给结构体分配内存规则

1,068 阅读6分钟

我们大家都知道 OC 类的本质是结构体, 那么如果我们想搞清楚 OC 对象的内存分配规则, 就必须先搞清楚结构体的内存分配规则.所以今天先来探索结构体的内存分配规则, 属性的类型可以是对象, 同样结构体的成员也可以是结构体, 就是结构体嵌套, 按照国际惯例我们先从简单的着手探索, 然后再探索复杂的情况.

设备情况:

  • Mac 系统, Intel 64位
  • IDE: Xcode

约定:

长度: 指类型被分配内存的大小, 或者 变量被分配内存的大小; 普通成员: 基本数据类型, 为能再分解为简单数据类型; 叶子成员: 成员或者嵌套结构体中结构体成员的成员, 直到不能再分解为其他数据类型,

分类: 按照复杂程度咱们自己暂时给分为两种情况:

  1. 基本结构体: 无嵌套, 成员都是基本数据类型
  2. 嵌套结构体: 有成员是结构体类型的, 可以多级嵌套, 这里只探索二级嵌套的情况.

基本结构体:

  1. 定义6个结构体, Struct4Struct5 这两个结构体成员相同, 但是成员声明的顺序不同.
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;

  1. 然后打印他们的长度.
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));
}

打印结果如下图, 记为图一: 图一.png

  • 他们长度分别是 1, 2, 4, 8, 16, 24;
  • Struct4Struct5, 他们各成员的长度总和是 1+2+4+8=15, 而他们的长度分别是 1624, 成员相同, 长度不同;
  • Struct3 各成员长度总和为7, 他的长度是 8;

由此我们得出两个结论:

  1. 结构体长度大于或者等于所有成员长度总和.
  2. 两个结构体, 成员相同, 成员在结构体的先后顺序不同, 两个结构体长度也有可能不同.
  • 这个大家可以测试一下, 有相同的情况存在, 比如 shortchar 两个成员.

成员相同 : 指同数据类型的成员数量相同

因此我们可以大胆猜测, 系统给结构体分配内存的时候, 肯定有一套规则, 按照这个规则去计算和分配内存. 既然跟顺序有关, 我们就按照顺序打印一下各成员的地址看看, 因为十六进制看着不太习惯, 所以同时输出了十进制数, 看起来更顺眼一些.

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

打印结果如下图, 记为图二: image.png

我们根据打印结果来绘制 2 张表格, 来观察一下有什么规律. 地址位数太多, 前边我就给省略了. 方便观察

  • s4的内存结构 image.png

  • s5的内存结构 image.png

我们通过这两个表格和图一中打印结果的观察发现:

  1. 结构体的首地址就是第一个成员的地址;
  2. 所有成员的地址都是偶数, 但是并不是按照偶数地址去做内存分配的,
  • s5 的成员 b 就可以看出, 并没有从编号 22 开始.
  1. 所有成员的地址相对于首地址的偏移量都是该成员长度的整数倍;
  2. 结构体被分配内存大小是最大成员长度的整数倍.

结论一:

系统给基本结构体分配内存时,每个成员的地址相对首地址的偏移量是这个成员长度的整数倍,总长度为最大长度成员的整数倍, 结构体的首地址就是第一个成员的地址;

嵌套结构体

定义三个结构体并打印各自长度 和 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));
}

打印结果如下图, 记为图三:

image.png

这个比较简单, 所以就直接在截图上标出来了, 做表格太麻烦, 各位理解万岁吧😄.按照结论一给出的结果来看看是否符合.

  1. Struct6 的长度是 16;
  2. Struct7 的长度应该都是 16 的整数倍, 但是实际上不是;
  3. 成员b的首地址偏移量是8, 也不是16的整数倍, 而是8的位数, Struct7 中成员长度最大是16, 但是结构体成员b 的成员最大长度是8;
  4. 结构体成员 b 的大小仍然是 16, 这和单独声明一个变量是相同的.
  5. 总长度是24, 也不是16的整数倍, 如果是按4的倍数,20就可以, 很显然不是, 24 明显是be长度的整数倍;

结论二:

系统给嵌套结构体分配内存时,普通成员的地址相对首地址的偏移量是这个成员长度的整数倍,结构体成员的首地址是其长度最长的成员的整数倍, 嵌套情况依次类推; 总长度为所有叶子成员中最大长度的整数倍, 结构体的首地址就是第一个成员的地址;

最终总结:

系统给结构体分配内存时的规则

  1. 结构体的成员,第一个成员地址相对于结构体首地址偏移量为 0, 即结构体首地址,
  2. 从第二个成员开始, 在前一个成员空间地址之后(即>=),普通成员首地址是该成员长度的整数倍; 结构体成员首地址是他所有叶子成员长度中最大长度的整数倍.
  3. 总长度是所有叶子成员长度最大长度的整数倍.

注意: sizeof是操作符, 而不是函数. 检测的是数据类型长度.

其实这些例子还不是很充足, 有些情况有可能没有展示出来, 大家可以多测试, 欢迎多交流, 共同进步.