首先我们来看一段代码:
struct MyStructOne {
char a;//1字节
short b;//2字节
int c;//4字节
long d;//8字节
} myStruct1;
struct MyStructTwo {
int c;//4字节
long d;//8字节
char a;//1字节
short b;//2字节
} myStruct2;
NSLog(@"%lu ----- %lu",sizeof(myStruct1),sizeof(myStruct2));
打印结果如下:
我们看到这两个结构体里面的内容内容一致,只是a、b顺序放到了下面,为什么就多占用了8个字节?并且结构体里面的变量总占用内存其实只要15字节,为什么多占用了一个字节?这是因为出现了内存对齐。
内存对齐规则
每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16
来改变这一系数,其中的n就是你要指定的“对齐系数”。在iOS中,Xcode默认为#pragma pack(8)
,即8字节对齐
1.数据成员对齐规则:结构(struct)(或联合(union))的数据成员,第一个数据成员
放在offset为0
的地方,以后每个数据成员存储的起始位置要从该成员大小或者成员的子成员大小
(只要该成员有子成员,比如说是数组,结构体等)的整数倍开始
(比如int为4字节,则要从4的整数倍地址开始存储)。
2.结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部最大元素大小的整数倍
地址开始存储。(struct a里存有struct b,b里有char,int ,double等元素,那b应该从8的整数倍开始存储。)
3.收尾工作:结构体的总大小,也就是sizeof的结果,必须是其内部最大成员的整数倍
,不足的要补⻬。
附上各类型内存占用表
举例验证
struct MyStructOne {
int c; //4字节 0 1 2 3
long d;//8字节 (4 5 6 7) 8-15
char a;//1字节 16
short b;//2字节 (17) 18 19
} myStruct1; //根据第三条对齐 24
struct MyStructTwo {
int c;//4字节 0 1 2 3
char a;//1字节 4
struct MyStructOne struct1; //24字节 (5 6 7) 8-31
short b;//2字节 32 33
} myStruct2; //根据第三条对齐 40
测试代码
long a = (long) &myStruct2.c;
long b = (long) &myStruct2.a;
long c = (long) &myStruct2.struct1.c;
long d = (long) &myStruct2.struct1.d;
long e = (long) &myStruct2.struct1.a;
long f = (long) &myStruct2.struct1.b;
long g = (long) &myStruct2.b;
NSLog(@"%lu ----- %lu",sizeof(myStruct1),sizeof(myStruct2));
NSLog(@"%ld ----- %ld ----- %ld",a,b,c);
NSLog(@"%ld ----- %ld ----- %ld ----- %ld",d,e,f,g);
最终结果如下:
根据打印结果证实与上面分析结果一致
-
myStruct1
与myStruct2
的内存大小分别是24
和40
- 结构体
myStruct2
首地址4294975744
,c
占用4
个字节,a
占1
个字节,打印地址符合 - 结构体
myStruct1
,内部最大元素long
占用8
字节,从偏移量8的地址开始存储
,4294975752
符合 - 结构体
myStruct1
内部元素符合,占用偏移量为8-31
的内存 - 结构体
myStruct2
最后的b元素,从偏移量32位置开始
(32刚好是2的整数倍),4294975776
符合;最后根据第三条,myStruct2
内部最大成员为myStruct1
里的long
类型,整个结构体内存8
字节对齐,故从33
扩容到40
字节
为什么要内存对齐
内存是一片连续的地址单元,cpu存取数据的时候是一块一块的进行存取的,块的大小可以是2,4,8,16字节大小。每次内存存取都会产生一个固定的内存开销,减少内存存取次数将可以提升程序的性能。所以cpu一般会以2/4/8/16/32字节为单位进行存取。上述这些存取单位也就是块大小被称为(memory access granularity)内存存取粒度。
总结
了解完这些之后,当我们再声明结构体的时候就可以合理安排内部数据的顺序,从而使其占用更少的内存。
:Vc,Vs等编译器默认是#pragma pack(8),所以测试我们的规则会正常;而gcc默认是#pragma pack(4),并且gcc只支持1,2,4对齐。套用三原则里计算的对齐值是不能大于#pragma pack指定的n值。