前言
结构体是c/c++比较常见的数据结构,研究它对于深入学习Object-C也是比较重要的一环。一般的结构体的内存构成不是属性连续的存储,这其中存在着内存对齐。为什么要对齐呢?其根本原因在于提高CPU读取内存的效率。
一、准备
1、C、OC基本数据类型占字节数对照表
C | OC | 32位 | 64位 |
---|---|---|---|
bool | BOOL(64位) | 1 | 1 |
signed char | (__signed char)int8_t、BOOL(32位) | 1 | 1 |
unsigned char | Boolean | 1 | 1 |
short | int16_t | 2 | 2 |
unsigned short | unichar | 2 | 2 |
int int32_t | NSInteger(32位)、boolean_t(32位) | 4 | 4 |
unsigned int | boolean_t(64位)、NSUInteger(32) | 4 | 4 |
long | NSInteger(64位) | 4 | 8 |
unsigned long | NSUInteger(64位) | 4 | 8 |
long long | int64_t | 8 | 8 |
float | CGFloat(32位) | 4 | 4 |
double | CGFloat(64位) | 8 | 8 |
pointer | pointer | 4 | 8 |
2、三条对齐原则
① 、数据成员对⻬规则:结构(struct)(或联合(union))的数据成员,第
一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要
从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,
结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存
储。
②、结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从
其内部最大元素大小的整数倍地址开始存储.(struct a里存有struct b,b
里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
③、收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大
成员的整数倍.不足的要补⻬。
二、分析
1、我们先来看两个简单的结构体
struct LGStruct1 {
double a;
char b;
int c;
short d;
}struct1;
struct LGStruct2 {
double a;
int b;
char c;
short d;
}struct2;
struct LGStruct3 {
double a;
int b;
struct LGStruct2 sub_struct;
char c;
short d;
}struct3;
2、手动分析(都是从0字节开始分析)
-
对于结构体
LGStruct1
: =>algin(?+ ? + ? + ?)分:计算a、b、c、d属性
a:第一个数据,从0开始排列,由于是double类型,根据上面的对照表需要8个字节,所以内存字节序号区间为[0,7];=>algin(8+ ? + ? + ?) b:根据对照表知道它需要1个字节,有根据上面的原则①知道,它应该放在8号位;=>algin(8+ 1 + ? + ?) c:需要4个字节,由于原则①,9、10、11号位都不能填数据,所以对饮内存字节序号为[12,15];=>algin(8+1+(3+4)+?) d:需要2个字节,由于原则①,对应内存字节序号区间为[16,17];=>algin(8+1+(3+4)+2);
合:但是,algin(18)需要根据原则③,进行总体内存对齐;我们知道
LGStruct1
里面a数字所需字节数为8,是最多的,8大于18的最小倍数是24,所以LGStruct1
结构体产生的实例struct1所需内存大小为24 对于结构体
LGStruct2
,我们应用上面相同的流程计算可以得到其内存所需大小为16-
说了这么多都不如运行一下验证我们的推理来的实在,运行结果如下
怎么样,是不是一模模,一样样;看来我们的过程初步来看是没有问题的。
-
接下来我们来推一下
LGStruct3
,结构体中嵌套结构体;=>algin(? + ? + ? + ? + ?)
分: a, b,sub_struct, c, d属性计算a : 需要8字节,对应区间[0,7]; => algin(8+ ? + ? + ? + ?) b : 需要4字节,对应区间[8, 11]; => algin(8+ 4 + ? + ? + ?) sub_struct: 需要16字节,由于原则②,对应的区间为[16, 31];=> algin(8+ 4 + (4 + 16) + ? + ?) c : 需要1字节,区间[32,32]; => algin(8+ 4 + (4 + 16) + 1 + ?) d : 需要2字节,区间[34, 35];=> algin(8+ 4 + (4 + 16) + 1 + (1 + 2))
总: 因为内存最大成员为
double
8字节,所以algin(36) = 40,我们看看运行结果吧!
看来这一步应该也是正确的。
3、我只到现在我们手动分析了一下,肯定不能满足我们的求知欲;难道大家就没有想过:在内存中结构体真的是按照我们推理的这样排列的吗?不符就干,上图。我们来动态调试分析
- 我们通过
lldb
调试 打印struct 的首地址p &struct1
: 0x000000010b69d9a0; - 我们来x/8gx 一下这个首地址:0x000000010b69d9a0;得到一下内容
0x10b69d9a0: 0x3ff0000000000000 0x0000000300000061
0x10b69d9b0: 0x0000000000000004 0x3ff0000000000000
0x10b69d9c0: 0x0005006200000002 0x0000000000000000
0x10b69d9d0: 0x0000000000000000 0x0000000000000000
1)第一个双字(QW): 0x3ff0000000000000 ,对应的应该就是struct1
中的a
的内存内容
00111111 11111111 00000000 00000000 00000000 00000000 00000000 00000000 //内容已经大端排列, 二进制展示
对应的double类型
符号位:0
指数部分:1023 - 1023 = 0
尾数部分:0
为什么这么分解请参照浮点类型
求解出来不就是1吗?这里对应的字节区间为[0,7]
-
第二个双字(QW): 0x0000000300000061,拆分到内存字节序为
<61, 00, 00, 00, 03, 00, 00, 00> //内容已经小端排列,16进制展示
这里肯能就会疑惑,为什么上一个double类型分析,大端排列,这里又小端排列?
答:double类型分析,分析的是已经是double类型整体8字节内容。这里小端
是内存的字节序,也就叫主机序
(不一样的cpu可能不一样的主机序
啊,现在一般都是小端
),我们是要进行拆分来分析的。
看0x61
不就是字符a
的ascii
吗,对应的就是struct1
中的b
属性,这是第8号位,区间[8,8];接下来的区间[9,11]可以看到都是存放的0x00
,到了12号位才有数据;我们知道接下来我们需要读取一个int
4字节内容
也就是区间[12, 15]内容<03, 00, 00, 00>
转换到大端排列,就是数值为3的struct1
中的c
中的值。
3)第三个双字(QW): 0x0000000000000004,拆分到内存字节序为
<04, 00, 00, 00, 00, 00, 00, 00> //内容已经小端排列,16进制展示
我们知道我们接下来要读一个2字节的short
,需要读取的内存为<04, 00>, 现在我们应该清晰的认识到这个值为4)对应的是struct1
中的d
中的值。
5)第4个双字:0x3ff0000000000000对应struct2
中的a
的值;
6)第五个双字:0x0005006200000002
<02, 00, 00, 00, 62, 00, 05, 00>
根据前序步骤,聪敏的你们一定可以分析出这里的内容分别是
struct2.b = 2;
struct2.c = 'b';
struct2.d = 5;
三、 总结
结构体的内存布局,抓住3个原则,就可以递归的分析所有的结构体了。了解了结构体的内存结构,有助于我们理解Object-C进行编译的时候为什么会属性重排,其实就是为了提高存储能力。