一、结构体中内存对齐
1.1 对齐规则
- 结构体第一个成员变量对齐相对于结构体成员地址偏移量为0的位置上
- 其他成员变量需要对齐到对齐数的整数倍
- 结构体总大小为最大对齐数的正数倍
如果存在嵌套结构体的情况,嵌套结构体占用空间需要对齐自身最大对齐数的整数倍,同时在计算结构体总大小的时候,嵌套结构体的最大对齐数参与比较
【注意】:对齐数 == 编译器默认的一个对齐数与该成员变量大小的较小值
- 在vs环境下,系统默认对齐为8
- 在Linux中没有默认对齐数,对齐数就是成员自身的大小
通过题目熟练的掌握以上知识.
struct S1 { char c1; int i; char c2; }; printf("%d\n", sizeof(struct S1));--12 struct S2 { char c1; char c2; int i; }; printf("%d\n", sizeof(struct S2));--8 struct S4 { char c1; struct S2 s2; double d; }; printf("%d\n", sizeof(struct S2));--24
【说明】:数值代表的是结构体变量地址处的偏移量
1.2 内存对齐的意义
⼤部分的参考资料都是这样说的:
平台原因(移植原因):
- 不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常
性能原因:
- 数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;对齐的内存访问仅需要⼀次访问。
假设⼀个处理器总是从内存中取8个字节,则地址必须是8的倍数。如果我们能保证将所有的double类型的数据的地址都对齐成8的倍数,那么就可以用一个内存操作来读或者写值了。否则,我们可能需要执行两次内存访问,因为对象可能被分放在两个8字节内存块中。
总体来说:结构体的内存对齐是拿空间来换取时间的做法
通过上述的观察,不难看出。如果不存在内存对齐,需要执行两个内存访问(对象被分放在两块内存块),而内存对齐只需要进行一次。
对此在涉及结构体时,需要考虑满足对齐,又要节省空间。可以将占用空间小的成员尽量集中在一起
struct S1 { char c1; int i; char c2; }; struct S2 { char c1; char c2; int i }; S2 < S1
1.3 #pragma(预处理指令)
1.3.1 pragma相关介绍
- 用于指定计算机或操作系统特定的编译器功能
- 根据定义
pragma
指令是计算机或操作系统特定的,并且通常对于每个编译器而言都有所不同 - pragma指令可用于条件语句以提供新的预处理器功能,或为编译器提供实现所定义的信息,
1.3.2 #pragma pack(n)修改默认对齐数
#include <stdio.h> #pragma pack(1)//设置默认对齐数为1 struct S { char c1; int i; char c2; }; #pragma pacK()//取消默认对齐数,还原为默认对齐数 int main() { printf("%d\n",sizeof(struct S)); return 0; }
推荐使用场景,在结构体进行内存对齐时,如果对于对齐方式不能达到预期,可以通过该指令更改默认对齐数
获得该成员变量的偏移量
这里需要使用一个函数offsetof()宏,该函数被声明在stddef.h
文件中,以下是函数offsetof()宏
size_t offsetof(type,member);
【宏定义】:
#define offsetof(TYPE,MEMBER) ((size_t)&((TYPE*)0)->MEMBER)
如果想要了解更多,可以参考下这篇博客Offsetof宏详解-CSDN博客.这里只如何去使用Offsetof()宏计算出结构体某成员地址的偏移量。
#include <stdio.h> #include <stddef.h> struct S { char a; int i; }; int main() { printf("%d\n",offsetof(struct S,i)); //那么这里的结果就是就是4 return 0; }
【小总结】:
结构体中的内存对齐是为了以空间换取时间的做法,随着计算机不断地更新换代,一般不需要担心内存空间不足的问题,逐渐地从更多考虑的是时间上的问题。同时为了节约空间的开销,提出位段
二、结构体实现位段
2.1 位段的概念
位段是结构体的一种变形,在功能、用法上与结构体基本一致,但是在于内存分配上不同,位段可以很好的节省空间,可存在位段跨平台的问题。同时与结构体相比有两个点不同。
- 成员上:
int
、unsigned int
或signed int
,但是在C99中是可以选择其他类型 - 格式上:位段成员名后面有一个冒号和一个数字
struct A { char _a:2; char _b:5; };
【说明】:这里数字代表的是该成员变量占用空间大小,而大小单位是比特
【问题】:位段A所占的内存大小是多大?
这个问题,需要利用下面的知识了
2.2 位段的内存分配
- 位段成员:
int
、unsigned int
、signed int
或者char
等类型(需要是整形,是要转换为二进制) - 位段开辟空间的大小一般是以四个字节或一个字节开辟的
- 位段涉及许多不确定的因素,位段是不跨平台的,注意可移植的程序,应该避免使用位段
struct S { char a:3; char b:4; char c:5; char d:4; }; struct S s = {0}; s.a = 10; s.b = 12; s.c = 3; s.d = 4;
2.3 位段的跨平台问题
【不确定的因素大致包括】:
- 内存存放的方向是从左到右,还是从右到左
- 是低地址到高地址,还是高地址到低地址
- int类型是不确定是被当作有符号数还是无符号数
- 当一个结构体包括了两个位段,第二个位段比较大,无法容纳第一个位段剩下的空间,是舍弃还是利用剩下的空间,这是不确定的
- 位段中最大位的数目不能确定(16位机器最大16,32位机器最⼤32,写成27,在16位机器会出问题),可能会冲出最大的范围,出现问题
我们不妨以vs2013环境下测量下数据
vs2013下,位段是从左到右,从低地址到高地址,位段需要的空间不足,直接开辟一块新的空间,我们来结合图片理解下
【步骤】:
- 位段开辟八个bit位(这里是char类型的情况)
- 位段成员后面数字是占用多少bit位
- 根据变量数据,转化为二级制(一个二级制为一个比特位),根据位段对应的数据,将转为的二级制多个比特位放入
- 关于上不确定因素中(4),vs2013选择舍弃,那就开辟一块新的空间,重复(1,2,3)步骤
2.4 位段的应用
比如下图中网络协议中,在一个结构存在很多只需要几个bit位就能实现的效果,这里使用位段就能达到想要的效果,也能节省空间的浪费。同时网络传输的数据大小也会小一点,提高了网络的流畅和效率!
【位段使用注意事项】:
struct A { int _a:2; int _b:5; }; int main() { //错误的做法 struct A s={0}; scanf("%d",&s._a); //正确的示范 int b=0; scanf("%d",&b); s._b=b; return 0; }
【说明】:
位段的几个成员共有同一个字节,而有些成员的起始位置并不是某个字节的起始位置。对此这些位置是没有地址(内存中每个字节分配一个地址,一个字节内部的bit位是没有地址的)
【解决办法】:
可以将值放入一个变量中,再通过赋值给位段成员,这个赋值在以后的操作中,是很巧妙的用法的。