引言
C
语言结构体内存布局是一个老生常谈的问题,网上也看了一些资料,有些说的比较模糊,有些是错误的。本人借鉴了前人的文章,经过实践,总结了一些规则,如有错误,希望指正,不胜感激。
实际环境
-
系统环境
macOS Sierra(10.12.4)
-
IDE
Xcode(8.3)
概述
影响结构体内存布局有
位域
和**#pragma pack预处理宏**两个情况,下面分情况说明。
正常情况
结构体字节对齐的细节和具体的编译器实现相关,但一般来说遵循3个准则:
-
结构体变量的首地址能够被其最宽基本类型成员的大小(
sizeof
)所整除。 -
结构体每个成员相对结构体首地址的偏移量
offset
都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。 -
结构体的总大小
sizeof
为结构体最宽基本成员大小的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
下面的demo会为大家解释以上规则:
代码
struct student {
char name[5];
double weight;
int age;
};
复制代码
struct school {
short age;
char name[7];
struct student lilei;
};
复制代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
struct student lilei = {"lilei",112.33,20};
printf("size of struct student: %lu\n",sizeof(lilei));
printf("address of student name: %u\n",lilei.name);
printf("address of student weight: %u\n",&lilei.weight);
printf("address of student age: %u\n",&lilei.age);
struct school shengli = {70,"shengli",lilei};
printf("size of struct school: %lu\n",sizeof(shengli));
printf("address of school age: %u\n",&shengli.age);
printf("address of school name: %u\n",shengli.name);
printf("address of school student: %u\n",&shengli.lilei);
}
return 0;
}
复制代码
输出结果
解释规则
-
编译器在给结构体开辟空间时,首先找到结构体中最宽的基本数据类型,然后寻找内存地址能被该基本数据类型所整除的位置,做为结构体的首地址。(在本demo中
struct school
包含
struct student
,所以最宽的基本数据类型为
double
,
sizeof(double)
为
8
,
1606416152/8 = 200802019
,
1606416112/8 = 200802014
)。 -
为结构体的每一个成员开辟空间之前,编译器首先检查预开辟空间首地址相对于结构体首地址的偏移是否是本成员大小的整数倍,若是,则存放本成员,反之,则在本成员和上一个成员之间填充字节,以达到整数倍的要求,也就是将预开辟空间的首地址后移几个字节(这也是为什么
struct student weight
成员的首地址是
1606416160
而不是
1606416157
,**但有很重要的一点要注意,这里的成员为基本数据类型,不包括char类型数组和结构体成员,char类型数组按1字节对齐,结构体成员存储的起始位置要从自身内部最大成员大小的整数倍地址开始存储,**比如
struct a
里有
struct b
成员,
b
里有
char,int,double
等成员,那
b
存储的起始位置应该从8的整数倍开始。通过
struct school
成员内存分布可以看出来,
school.name
的首地址是
1606416114
,而不是
1606416119
,
school.student
的首地址是
1606416128
,能被
8
整除,不能被
24
整除)。 -
结构体的总大小包括填充字节,最后一个成员出了满足上面两条之外,还必须满足第三条,否则必须在最后填充一定字节以满足要求(这也是为什么
struct student
占用字节数为
24
而不是
20
的原因)。
内存分布
扩展
细心的朋友可能发现
&shengli.lilei
(等效于
shengli.lilei.name
)的数值并不等于
lilei.name
,也就是说
struct school shengli
里的成员
struct student lilei
和
struct student lilei
并不是指向同一块内存空间,是值拷贝开辟的一块新的内存空间,也就是说struct是值类型而不是引用类型数据结构。还有通过内存地址可以发现两个结构体变量的内存空间是在内存栈上连续分配的。
位域
结构体使用位域的主要目的是压缩存储,位域成员不能单独被取
sizeof
值。C99规定
int,unsigned int,bool
可以作为位域类型,但编译器几乎都对此做了扩展,允许其它类型存在。
结构体中含有位域字段,除了要遵循上面3个准则,还要遵循以下4个规则:
-
如果相邻位域字端的类型相同,且位宽之和小于类型的
sizeof
大小,则后一个字段将紧邻前一个字段存储,直到不能容纳为止。 -
如果相邻位域字段的类型相同,但位宽之和大于类型的
sizeof
大小,则后一个字段将从新的存储单元开始,其偏移量为其类型大小的整数倍。 - 如果相邻的位域字段的类型不同,则各编译器的具体实现有差异,VC6采取不压缩方式,Dev-C++采取压缩方式。
- 如果位域字段之间穿插着非位域字段,则不进行压缩。
下面的demo会为大家解释以上规则:
代码
typedef struct A {
char f1:3;
char f2:4;
char f3:5;
char f4:4;
}a;
复制代码
typedef struct B {
char f1:3;
short f2:13;
}b;
复制代码
typedef struct C {
char f1:3;
char f2;
char f3:5;
}c;
复制代码
typedef struct D {
char f1:3;
char :0;
char :4;
char f3:5;
}d;
复制代码
typedef struct E {
int f1:3;
}e;
复制代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
printf("size of struct A: %lu\n",sizeof(a));
printf("size of struct B: %lu\n",sizeof(b));
printf("size of struct C: %lu\n",sizeof(c));
printf("size of struct D: %lu\n",sizeof(d));
printf("size of struct E: %lu\n",sizeof(e));
}
return 0;
}
复制代码
输出结果
解释规则
-
struct A
中所有位域成员类型都为
char
,第一个字节只能容纳
f1
和
f2
,
f3
从下一个字节开始存储,第二个字节不能容纳
f4
,所以
f4
也要从下一个字节开始存储,因此
sizeof(a)
结果为
3
。 -
struct B
中位域成员类型不同,进行了压缩,因此
sizeof(b)
结果为
2
(不压缩方式没有进行验证,很抱歉)。 -
struct C
中位域成员之间有非位域类型成员,不进行压缩,因此
sizeof(c)
结果为3。 -
struct D
中有无名位域成员,
char f1:3
占
3
个
bit
,
char :0
移到下
1
个字节(移动单位和具体位域类型有关,
short
移到下
2
个字节,
int
移到下
4
个字节),
char :4
占
4
个
bit
,然后不能容纳
char f3:5
,所以要存到下
1
个字节,因此
sizeof(d)
结果为
3
。 -
可能有人会疑惑,为什么
sizeof(e)
结果为
4
,不应该是只占用
1
个字节么?不要忘了上面提到的
准则3
。
注意事项
- 位域的地址不能访问,因此不允许将&运算符用于位域。不能使用指向位域的指针也不能使用位域的数组(数组是种特殊指针)。
- 位域不能作为函数的返回结果。
-
位域以定义的类型为单位,且位域的长度不能超过所定义类型的长度。例如定义
int a:33
是不被允许的。 -
位域可以不指定位域名,但不能访问无名的位域。无名的位域只用做填充或调整位置,占位大小取决于该类型。例如
char:0
表示整个位域向后推一个字节,即该无名位域后的下一个位域从下一个字节开始存放,同理
short:0
和
int:0
分别代表整个位域向后推两个和四个字节。当空位域的长度为具体数值N时(例如
int:2
),该变量仅用来占N位。
pragma pack预处理宏
编译器的
#pragma pack
指令也是用来调整结构体对齐方式的,不同编译器名称和用法略有不同。使用伪指令
#pragma pack(n)
,编译器将按照n个字节对齐,其取值为1、2、4、8、16,默认是8,使用伪指令
#pragma pack()
,取消自定义字节对齐方式。如果设置
#pragma pack(1)
,就是让结构体没有填充字节,实现空间“无缝存储”,这对跨平台传输数据来说是友好和兼容的。
结构体中含有#pragma pack预处理宏,除了要遵循上面3个准则,还要遵循以下2个规则:
-
对于结构体成员存放的起始地址的偏移量,如果n大于等于该成员类型所占用的字节数,那么偏移量必须满足默认的对齐方式,如果n小于该成员类型所占用的字节数,那么偏移量为n的倍数,不用满足默认的对齐方式。即是说,结构体成员的偏移量应该取二者的最小值,公式如下:
offsetof(item) = min(n, sizeof(item))
- 对于结构体的总大小,如果n大于所有成员类型所占用的字节数,那么结构的总大小必须为占用空间最大成员占用空间数的倍数,否则必须为n的倍数。
用法
#pragma pack(push) //packing stack入栈,设置当前对齐方式
#pragma pack(pop) //packing stack出栈,取消当前对齐方式
#pragma pack(n) //n=1,2,4,8,16保存当前对齐方式,设置按n字节对齐
#pragma pack() //等效于pack(pop)
#pragma pack(push,n)//等效于pack(push) + pack(n)
复制代码
代码
#pragma pack(4)
typedef struct F {
int f1;
double f2;
char f3;
}f;
#pragma pack()
复制代码
#pragma pack(16)
typedef struct G {
int f1;
double f2;
char f3;
}g;
复制代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
printf("size of struct D: %lu\n",sizeof(f));
printf("size of struct E: %lu\n",sizeof(g));
}
return 0;
}
复制代码
输出结果
解释规则
-
struct F
设置的对齐方式为
4
,
min(4, sizeof(int)) = 4
,
f1
占
4
个字节,偏移量为
0
,
min(4, sizeof(double)) = 4
,
f2
占
4
个字节,偏移量为
4
,
min(4, sizeof(char)) = 1
,
f3
占
1
个字节,偏移量为
12
,最后整个结构体满足
准则3
,
sizeof(f) = 16
。 -
struct G
设置的对齐方式为
16
,比结构体中所有成员类型都要大,相当于没有生效,因此
sizeof(f) = 24
。
总结
位域
和**#pragma pack预处理宏
的结构体在遵循
3个准则**的前提下,有自己的相应规则也要遵守。结构体成员在排列时数据类型要遵循从小到大排列,这样能尽可能的节省空间。
参考链接
blog.sina.cn/dpool/blog/…
c.biancheng.net/cpp/html/46…
hubingforever.blog.163.com/blog/static…