5、结构体内存对齐

1.内存对齐

有过计算机组成原理和操作系统学习经验的同学可以知道,在现实中并不会出现像题目中‘人工智能’般的内存申请、分配,不可能不同类型的数据一个个挨着紧密无间不留一点空隙,这样对于机器来说是无法快速读取的而且对于开发也不好拓展,因此机器需要一个规则来存放和读取内存数据,内存对齐原则就是这种类型的规则。

2.数据类型所占大小

在iOS中,每个不同类型的数据都规定了相应的内存大小:

常用数据类型 32位机器(4字节) 64位机器(8字节)
char 1 1
BOOL 1 1
short 2 2
int 4 4
unsigned int 4 4
long 4 8
unsigned long 4 8
long long 8 8
unsigned long long 8 8
float 4 4
double 8 8
NSInteger 4(=int) 8(=long)
NSUInteger 4=(unsigned int) 8(=unsigned long)
point 4 8

对于指针类型来说64位机上占8字节,但是这个8字节并不是指指针所指内存大小,而是存贮这个指针内存的大小是固定8字节。而其他值类型就如表所示大小。
表中并没有给出Struct内存大小,因为结构体由于内部成员数目和类型都不缺定,所以不能给出定值,那它是否是所有成员占字节数目之和呢,按照第一节的介绍,也不可能是简单的求和,按照alloc分析、源码调试(3)分析的结论,提出猜想:ios对于Struct的内存占用规则是以8进制或者16进制对齐来实现。

3.结构体内存申请的探究

猜想需要验证,那就来验证一波,上代码。

struct PPBox{
    char a;      //1
    double width; //8
    int length; //4
    short height; //2
}PPBox;

struct PPWindow{
    double width;  //8
    int length;   //4
    short height;   //2
    char a;  //1
}PPWindow;

struct PPBox box;
box.a = 'a';
box.width = 1.0;
box.length = 2.0;
box.height = 3.0;
        
struct PPWindow window;
 //24       
NSLog(@"%lu",sizeof(box));
 //16        
NSLog(@"%lu",sizeof(window));

可以得到打印结果分别是24和16。同样的数据类型只是顺序变了就导致结构体内存大小不一样,但不管是24还是16都不是 double(8字节)+int(4字节)+short(2字节)+char(1字节) = 15字节这种简单求和方式,对于文章第一条对齐的方式确实存在是可以验证的。
其次,可以增删改数据类型后会发现是以8的倍数出现,故这个结构体内存对齐是以8字节对齐的,这个也是可以确定的。
根据网上搜索可以得到结构体内存对齐的规则有如下三点:

1:结构(struct)的数据成员,第一个数据成员放在offset为0的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储。 min(当前开始的位置m,n) m=9 n=4 9 10 11 12。
2:结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍地址开始存储。(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储.)
3:收尾工作:结构体的总大小,也就是sizeof的结果,.必须是其内部最大成员的整数倍.不足的要补⻬。

看不懂,太麻烦,那我直接在这里对PPBox进行验证:
PPBox
第一个是char a占1个字节,offset变为1,
第二个是double width占8字节,当前offset为1,8%1=0,所以double width存储的起始位置1,占8字节,offset变为9。
第三个是int length占4字节,当前offset为9,9%4 = 1不为0,所以offset+1继续判断,当offset为12时可以整除,即起始位置为12,占4字节,offset变为16。
第四个是short height占2字节,当前offset为16,可以整除2,所以可以直接存放,offset变为22。
第一步结束,第二部不涉及到,第三部收尾22要是最大成员double width8字节整数倍,因此补充至24。可以的,没毛病~。

PPWindow进行验证:
PPWindow
第一个是double width占8字节,直接存放,offset变为7。
第二个是int length占4字节,7%4=3不为0,offset+1,当offset为8时可以整除,故以这个位置开始存放,offset变为12。
第三个是short height占2字节,7%4=3不为0,offset+1,当offset为8时可以整除,故以这个位置开始存放,offset变为12。
第四个是char a占1字节,12可以整除,故以这个位置开始存放,offset变为13。
第一步结束,第二部不涉及到,第三部收尾13要是最大成员double width8字节整数倍,因此补充至16。可以的,也没毛病~。
后续也可以验证下不同类型成员时是否满足。

4.结构体嵌套内存对齐

由于3条原则中有一条是针对嵌套类型的结构体的内存对齐的,因此也需要研究一下这个情形的,鉴于之前有代码首先偷个懒:

struct PPBox{
    char a;
    double width;
    int length;
    short height;
}PPBox;

struct PPWindow{
    double width;
    int length;
    short height;
    char a;
}PPWindow;

struct PPBox1{
    char a;
    double width;
    int length;
    short height;
    struct PPWindow window;
}PPBox1;

struct PPWindow1{
    double width;
    int length;
    short height;
    char a;
    struct PPBox box;
}PPWindow1;
        
NSLog(@"%lu",sizeof(box));
        
NSLog(@"%lu",sizeof(window));

运行输出新的两个结构体都是40,上一章3就知道,原版PPBox是24字节,PPWindow是16字节,40=24+16,so easy?
重新写个再验证一下:

struct PPMiniBox{
    int qrCode;
    struct PPBox box;
}PPMiniBox;

PPBox中最大字节数是double widthPPMiniBox中只有一个int qrCode最大4字节,最终输出的结果是32,走一遍规则试试:
PPMiniBox
第一个是int qrCode占4字节,offset变为4,
PPBox中第一个是char a占1个字节,4可以整除,但原则二中起始地址必须是最大字节数的整数倍,即8的整数倍,offset变为8,存1字节变为9,
PPBox中第二个是double width占8字节,当前offset为9,9%8不为0,所以offset+1,知道变为16可以整除,存放,占8字节,offset变为24。
PPBox中第三个是int length占4字节,当前offset为24,24%4 = 0,所以offset为24可以存放,占4字节,offset变为28。
PPBox中第四个是short height占2字节,当前offset为28,可以整除2,所以可以直接存放,offset变为30。
第一步完成,第二步也遵循了,第三步收尾,最大字节数为8,offset变为整数倍就变为32。
与原则对应~

5.实际内存的分析

先对结构体赋值:

struct PPBox{
    char a;      //1
    double width; //8
    int length; //4
    short height; //2
}PPBox;

struct PPWindow{
    double width;  //8
    int length;   //4
    short height;   //2
    char a;  //1
}PPWindow;

struct PPBox box;
box.a = 'a';
box.width = 1.0;
box.length = 2.0;
box.height = 3.0;
  
struct PPWindow window;
window.a = 'b';
window.width = 4.0;
window.length = 5.0;
window.height = 6.0;

PPBoxPPWindow的真实内存情况分析:

lldb打印内存情况
分析内存特点:
PPBox:总计占用24字节。
首先,box中的第一个是char a可以知道'a'的ASCII值是61,这里0x0000000000000061这个整个8字节存的就是char a ='a'这个值,window可以发现一个0x0062就是char a ='b'这个值。
其次,box的第4个8字节和window的第一个8字节是一样的值,通过左边的指针地址也和发现是同一块,所以这两个结构体内存是连着的。box占3个8字节,window占2个8字节。
box分析:第一个8字节,'a'就占了8字节,和之前的规则还是有些不同的,对齐的大多数字节空间主要是用来填补这里了。
第二个8字节,0x3ff0000000000000按照原理应该是表示double width = 1,这个值但是为什么是这样,这里涉及到double类型的移码,表示1的移码,同样的0x4010000000000000表示的是4的移码,移码是双精度浮点型的二进制保存形式,这个移码这里不展开,后续拓展。
第三个8字节,可以明显看见0x000000030x00000002两个值,但是为什么不是先0x000000020x00000003,这里就是涉及到内存的大小端存储模式:

大端模式:是指数据的高字节保存在内存的低地址中,而低子节数据保存在内存的高地址中。
小端模式:是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中。

0x00000003在结构体中排在0x00000002的后面相当于低字节,所以保存在低地址中,即0x7ffeefbff480-0x7ffeefbff488这段中,0x7ffeefbff489-0x7ffeefbff48f这段存0x00000002
同样的道理分析window
占16字节,
第一个8字节,0x4010000000000000表示的width值为4,同样是移码。
第二个8字节,这次我们采用小端读法,4字节int类型存放了5这个值,2字节'short'类型存放了6这个值,最后一字节char类型存放ASCII值是62即'b'这个值,刚刚好。

6.类的内存对齐探究

新建一个Person

@interface Person : NSObject
@property (nonatomic,assign)   int age;
@property (nonatomic,strong)   NSString *nickname;
@property (nonatomic,assign)   float height;
@property (nonatomic,strong)   NSString *name;
@end

Person *person =  [Person alloc];
person.age = 10;
person.nickname = @"pp";
person.height = 180.0;
person.name = @"ppext";
        
NSLog(@"%lu",sizeof(person));

通过isa的相关分析可以知道isa与对象、类的关系
,对象的第一个8字节必定存放的是isa指针。
大致可以估计一下占用内存大小isa 8字节,age4字节,nickname8字节,offset偏移至24,height8字节,name8字节,offset偏移至32,占用32字节。

lldb打印48字节内容
后面16字节均是空的,所以信息应该在前32字节中,之前isa与对象、类的关系,这边中知道首8字节是isa指针,其他自定义属性信息在后24字节中,对三个8字节强转打印:
强转打印
0x0000000100004070表示的是name属性,0x0000000100004090表示的是nickname属性,那不用猜强转显示不出来的就是ageheight整合了的,0x433400000000000a小端读取,0x0000000a就是10表示age0x43340000应该就是180的移码,至此分析完成,也符合规则。
但是这里也有个问题,内存存放属性的顺序和定义的并不一样,不是先nicknameheight的顺序不对,是iOS为了内存对齐,优化存储对属性顺序重排了,将两个4字节组合到一段8字节中,适当调整顺序。

你可能感兴趣的:(5、结构体内存对齐)