结构体内存对齐
我们首先定义两个结构体,分别计算他们的内存大小,并讨论内存对齐原理
struct LGStruct1 {
double a; // 0-7)
char b; // 1 [8 1] (8)
int c; // 4 [9 4] 9 10 11 (12 13 14 15)
short d; // 2 [16 2] (16 17)
}struct1;
// 内部需要的大小为: 15
// 最大属性 : 8
// 结构体整数倍: 24
// 15 -> 16
struct LGStruct2 {
double a; //8 (0-7)
int b; //4 (8 9 10 11)
char c; //1 (12)
short d; //2 13 (14 15) - 16
}struct2;
// 15 -> 16
//打印两个结构体内存大小
NSLog(@"%lu-%lu",sizeof(struct1),sizeof(struct2));
打印结果
从两个结构体来看两者没啥大的区别,只是定义的属性顺序有些不一样,但结果相差很大,这是为什么呢?是否内存大小的计算存在一定的规则呢?下面就来讨论一下这两个结构体内存大小的不同的原因:内存对齐的规则
内存对齐的规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16
来改变这一系数,其中的n就是你要指定的“对齐系数”。在ios中,Xcode默认为#pragma pack(8),即8字节对齐
。
- 数据成员对齐规则:
结构体(struct
)(或联合(union)
)的数据成员,第一个数据成员放在offset为0
的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小
(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始(比如int为4字节,则要从4的整数倍地址开始存储)。 - 结构体作为成员:如果
一个结构
里有某些结构体成员
,则结构体成员要从其内部最大元素的大小的整数倍地址开始存储
。(struct a
里存有struct b
,b
里有char
,int
,double
等元素,那b
应该从8
的整数倍开始存储)。 -
首尾工作:结构体的总体大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐。
数据类型对应的字节数表格
我们通过上面的代码来计算一下stuct1和struct2的大小:
struct LGStruct1 {
double a; // 0-7)
char b; // 1 [8 1] (8)
int c; // 4 [9 4] 9 10 11 (12 13 14 15)
short d; // 2 [16 2] (16 17)
}struct1;
解释:从数据类型对用的自己数表格可以知道double占8字节、char占1字节、int占4字节、short占2字节
,那么通过数据成员对齐原则
,我们知道
a
应该从0开始占8字节,那么a的内存范围[0-7]
;
b
是char类型占1字节,那么b的内存范围[8]
;
c
是int类型占4字节,那么c的内存范围本应该从9
开始计算,但是9
不是4
的整数倍,那么我们往后累加+1
,直到12是4的整数倍开始算占4位[12-15]
;
d是short类型占2字节,那么d的内存从16开始,16是2的整数倍,那么d的内存范围[16-17];
上面提到过内存对齐的三原则,按照结构体的总体大小,也就是sizeof的结果,必须是其内部最大成员的整数倍,不足的要补齐
,17不是8的整数倍,不足要补齐,那么LGStruct1
补齐后的内存大小为24。LGStruct2
同理可得内存大小为16
。
结构体嵌套结构体
首先我们定义一个结构体嵌套结构体例子
struct LGStruct3 {
double a; // 0-7)
char b; // 1 [8 1] (8)
int c; // 4 [9 4] 9 10 11 (12 13 14 15)
short d; // 2 [16 2] (16 17)
struct Mystruct{
double e; //8字节
short f; //1字节
}Mystruct;
}struct3;
我们按照如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素的大小的整数倍地址开始存储。(
struct a里存有struct b, b里有char,int,double等元素,那b应该从8的整数倍开始存储)原则来计算一下
LGStruct3的内存大小,从上面的代码我们已经知道了LGStruct3内存分配完d时为[16-17],按照规则,
Mystruct 应该从结构体内部的占位最大的字节整数倍开始算起,
Mystruct 中的
e为double占8字节,所以
Mystruct 应该从
18开始并且8的整数倍开始计算
e的内存分配情况为
[24-31],
f占2字节[32-33],所以
LGStruct3 实际占用的内存大小为
33,但是总大小要为其内部最大元素的大小整数倍即
>33&&是8的整数倍40`。
内存优化
LGStruct1
通过内存字节对齐原则,增加了9
个字节,而LGStruct2
通过内存字节对齐原则,通过4+2+1
的组合,只需要补齐一个字节即可满足字节对齐规则,这里得出一个结论结构体内存大小与结构体成员内存大小的顺序有关
。
如果是结构体中数据成员是根据内存从小到大的顺序定义的,根据内存对齐规则来计算结构体内存大小
,需要增加
有较大的内存padding即内存占位符,才能满足内存对齐规则,比较浪费内存。
如果是结构体中数据成员是根据内存从大到小的顺序定义的
,根据内存对齐规则来计算结构体内存大小,我们只需要补齐少量内存padding即可满足堆存对齐规则
,这种方式就是苹果中采用的,利用空间换时间,将类中的属性进行重排,来达到优化内存的目的
。
下面案例具体说明以上结论:
自定义一个类,并为这个类添加一些属性,我们通过断点调试,来查看属性的内存地址分配情况
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;
@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// 内存对齐
LGPerson *person = [LGPerson alloc];
person.name = @"Cooci";
person.nickName = @"KC";
person.age = 18;
person.c1 = 'a';
person.c2 = 'b';
NSLog(@"%@",person);
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
lldb调试结果
- 通过地址找出
name & nickName
,控制台有打印结果; - 通过地址查找
person.age、person.c1、person.c2
却显示的是乱码,这个是为什么呢?我们猜测这个是苹果进行了内存重排优化,下面我们来验证一下这个猜想;当我们向通过0x0000001200006261
地址找出age
等数据时,发现是乱码,这里无法找出值的原因是苹果中针对age、c1、c2属性的内存进行了重排
,因为age类型占4个字节
,c1和c2类型char分别占1个字节
,通过4+1+1
的方式,按照8字节对齐,不足补齐
的方式存储在同一块内存中。我们按照这个思路来打印一下结果 - age的读取通过
0x00000012
。 0x00000012 - c1的读取通过
0x62
0x62 - c2的读取通过
0x61
0x61
总结:大部分的内存还是按照顺序来分配的,苹果除了使用内存对齐规则外,会针对属性自行重排。
网友评论