计算结构体的大小(结构体内存对齐)、结构体实现位段
目录
- 结构体内存对齐
- 知识引入
- 对齐规则
- 为什么存在内存对齐
- 修改默认对齐数
- 结构体实现位段
- 什么是位段
- 位段的内存分配
- 位段的跨平台问题
- 位段使用的注意事项
结构体内存对齐
知识引入
为什么两个结构体的元素相同只是位置不同,计算出的在内存中的大小就不同呢?
这是因为结构体在内存中有自己的对齐规则
对齐规则
1.结构体的第一个成员对齐到和结构体变量起始位置偏移量为0的地址处
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处
对齐数=编译器默认的一个对齐数与该成员变量大小的较小值
VS中默认对齐数为8
Linux中gcc没有默认对齐数,对齐数就是成员自身的大小
3.结构体总大小为最大对齐数(结构体中每个成员变量都有一个对齐数,所有对齐数中最大的)的整数倍
4.如果嵌套了结构体的情况,嵌套的结构体成员对齐到自己的成员中最大对齐数的整数倍处
结构体的整体大小就是所有最大对齐数(含嵌套结构体中成员的对齐数)的整数倍
直接看这些概念还是很蒙的,我们还是通过代码来解释
首先介绍一个宏
offsetof – 宏
可以计算出结构体成员相较于起始位置的偏移量
offsetof (type,member)
头文件stddef.h
这样我们就知道了每个元素相对于起始位置的偏移量
现在我们通过画图来表示每个元素在结构体中的位置
我们发现即使把元素放在对应的偏移量处也才9个字节啊,为什么算出来的是12呢?
这就要用到我们的对齐规则了
到了这里你应该对结构体内存对齐有点想法了,趁热打铁,再看几道例题
//练习2
struct S1
{
char c1;
char c2;
int i;
};
//练习3
struct S3
{
double d;
char c;
int i;
};
//练习4
//结构体嵌套问题
struct S4
{
char c1;
struct S3 s3;
double d;
};
int main()
{
printf("%zd\n", sizeof(struct S1));
printf("%zd\n", sizeof(struct S3));
printf("%zd\n", sizeof(struct S4));
return 0;
}
大家算出来了嘛,这里为了让大家理解,每道题我都把它的图画出来
相信到了这里你一定完全掌握对齐规则了
那么为什么存在内存对齐呢?
我们直接一个字节挨着一个字节存不好吗?为什么弄得这么麻烦?
为什么存在内存对齐
⼤部分的参考资料都是这样说的:
1. 平台原因(移植原因):
不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定
类型的数据,否则抛出硬件异常。
2. 性能原因:
数据结构(尤其是栈)应该尽可能地在⾃然边界上对⻬。原因在于,为了访问未对⻬的内存,处理器需要
作两次内存访问;⽽对⻬的内存访问仅需要⼀次访问。假设⼀个处理器总是从内存中取8个字节,则地
址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对⻬成8的倍数,那么就可以
⽤⼀个内存操作来读或者写值了。否则,我们可能需要执⾏两次内存访问,因为对象可能被分放在两
个8字节内存块中。
总体来说:结构体的内存对⻬是拿空间来换取时间的做法。
那在设计结构体的时候,我们既要满⾜对⻬,⼜要节省空间,如何做到呢?
让占⽤空间⼩的成员尽量集中在⼀起
这个想必我们上面写的结构体S1和S2大家已经深有体会了
修改默认对齐数
#pragma这个预处理指令,可以改变编译器的默认对齐数
#pragma pack(1)//设置对齐数为1
struct S
{
char c1;
int a;
char c2;
};
#pragma pack()//取消设置的对齐数,还原为默认
int main()
{
printf("%zd\n", sizeof(struct S));
}
这里结构体的大小是多少呢?
我们还是画图说明
实际上这样算是直接取消对齐了,因为每个元素都是紧挨着放的
确实是6呢
结构体实现位段
什么是位段
位段的声明和结构是类似的,有两个不同:
1.位段成员必须是int、unsigned int或signed int,在C99中位段成员的类型也可以选择其他类型
2.位段的成员名后边有一个冒号和一个数字
代码演示
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
这里的A就是一个位段类型
那么位段所占内存的大小又是多少呢?
int main()
{
printf("%zd\n", sizeof(struct A));
return 0;
}
为什么会是8个字节呢?
这就涉及到位段的内存分配了
位段的内存分配
1.位段的成员可以是int、unsigned int、signed int、或者是char等类型
2.位段的空间上是按照需要以4个字节(int)或者1个字节(char)的方式来开辟的
3.位段涉及很多不确定因素,是不跨平台的,注重可移植的程序应该避免使用位段
画图解释位段的内存分配:两种情况
情况一:不浪费空间
情况二:浪费空间
这说明是第二种情况
但是还有一个问题,申请一个字节后是从前向后存储还是从后向前存储呢?
这里还是画图解释,假设是从后向前存储
这里是VS2022环境测试数据,到了这里大家肯定是知道位段怎么开辟空间了
但是位段的使用还是存在很多问题?比如跨平台问题。
位段的跨平台问题
- int位段被当成有符号数还是无符号数是不确定的
- 位段种最大位的数目不能确定。(比如int,16位机器最大是16,32位机器最大是32,
写成27在16位机器会出问题)。- 位段中的成员在内存中从左向右分配,还是从右向左分配,标准尚未定义。
- 当一个结构包含两个位段,第二个位段成员比较大,无法容纳于第一个位段
剩余的位时,是舍弃剩余的位还是利用,这是不确定的。
总结:
跟结构相比,位段可以达到同样的效果,并且可以很好的节省空间,但是有跨平台的问题存在。
位段使用的注意事项
位段的几个成员共有同一个字节,这样有些成员的起始位置并不是某个字节的起始位置,
那么这些位置处是没有地址的。内存中每个字节分配一个地址,字节内部的bit位是没有地址的。
所以不能对位段的成员使用&操作符,这样就不能使用scanf直接给位段成员输入值,
只能是先把值放在一个变量里,然后赋值给位段的成员。
代码举例:
struct A
{
int a : 2;
int b : 5;
int c : 10;
int d : 30;
};
int main()
{
struct A a = { 0 };
scanf("%d", &(a.b));//这是错误的
//正确的示范
int b = 0;
scanf("%d", &b);
a.b = b;
return 0;
}