C语言中结构体(struct)的详细分解与使用(中)https://developer.aliyun.com/article/1389335
第八:嵌入式开发中,C语言位结构体用途详解
在嵌入式开发中,经常需要表示各种系统状态,位结构体的出现大大方便了我们,尤其是在进行一些硬件层操作和数据通信时。但是在使用位结构体的过程中,是否深入思考一下它的相关属性?是否真正用到它的便利性,来提高系统效率?
下面将进行一些相关实验(这里以项目开发中的实际代码为例):
1. 位结构体类型设计
//data structure except for number structure typedef struct symbol_struct { uint_32 SYMBOL_TYPE :5; //data type,have the affect on "data display type" uint_32 reserved_1 :4; uint_32 SYMBOL_NUMBER :7; //effective data number in one element uint_32 SYMBOL_ACTIVE :1;//symbol active status uint_32 SYMBOL_INDEX :8; //data index in norflash,result is related to "xxx_BASE_ADDR" uint_32 reserved_2 :8; }SYMBOL_STRUCT, _PTR_ SYMBOL_STRUCT_PTR;
分析:这里定义了一个位结构体类型 SYMBOL_STRUCT,那么用该类型定义的变量都哪些属性呢?
看下面运行结果:
WORDS是定义的另一个外层类型定义封装,可以把它当作变量来看待。WORDS变量里前5个数据域的地址都是0x1ffff082c,而reserved_2的地址0x1fff0830,紧接着的PressureState变量是0x1fff0834。
开始以为:reserved_1 和 SYMBOL_TYPE 不在一个地址上,因为他们 5+4 共9位,超过了1个字节地址,但实际他们共用首地址了;而且reserved_2只定义了8位,竟然实际占用了4个字节(0x1fff0834 - 0x1fff0830),我本来是想让他占用1个字节的。
WORDS整体占了8个字节(0x1fff0834 - 0x1fff082c),设计时分析占用5个字节:
SYMBOL_TYPE 1个;reserved_1 1个;
SYMBOL_NUMBER+SYMBOL_ACTIVE 1个;
SYMBOL_INDEX 1个;reserved_2 1个;
uint_32 reserved_2 : 8; 占用4个字节,估计是uint_32在起作用,而这里写的8位,只是我使用的有效位数,另外24位空闲,如果在下面再定义一个uint_32 reserved_3 : 8,地址也是一样的,都是以uint_32为单位取地址。
同理,上面的5个变量,共用一个地址就不足为奇了。而且有效位的分配不是连续进行的,例如 SYMBOL_TYPE+reserved_1 共9位,超过了一个字节,索性系统就分配两个字节给他们,每人一个;SYMBOL_NUMBER+SYMBOL_ACTIVE 共8位,一个字节就能搞定。
2、修改数据结构,验证上述猜想
//data structure except for number structure typedef struct symbol_struct { uint_8 SYMBOL_TYPE :5; //data type,have the affect on "data display type" uint_8 reserved_1 :4; uint_8 SYMBOL_NUMBER :7; //effective data number in one element uint_8 SYMBOL_ACTIVE :1; //symbol active status uint_8 SYMBOL_INDEX :8; //data index in norflash,result is related to "xxx_BASE_ADDR" uint_8 reserved_2 :8; }SYMBOL_STRUCT,_PTR_ SYMBOL_STRUCT_PTR;
地址数据如下:
当换成uint_8后,可以看到地址空间占用大大减小,reserved_2 只占用1个字节(0x1fff069f - 0x1fff069e),其他变量也都符合上面的结论猜想。但是,注意看上面黄色和红色的语句,总感觉有些勉强,那么我又会想,前两个变量数据域是 9 位,那么他们实际上是不是真正的独立呢?虽然在 uint_8 上面他们是不同的地址,在uint_32 的时候是不是也是不同的地址空间呢?
3、分析结构体内部的数据域是否连续,看下图及结果
本来假设: 由前 2 次试验的结论,一共占用 8 个字节,节空间占用:(2+4)+(4+4)+(2+2+4)+(2+2)+(6)。可是,实际效果并不是想的那样。实际只占用了 4 个字节,系统并没有按照预想的方式,为 RESERVED 变量分配 4 个字节。
分析:
这些数据域,整体相加一共32位,占用4个字节(不考虑数据对齐问题)。而实际确实是占用了4个字节,唯一的原因就是:这些数据域以紧凑的方式链接,没有任何空闲位。实际是不是这样呢?
看下图和结果:
这里为了验证是否紧凑链接,用到了一个union数据,后面会讲到用union不会对数据组织方式有任何影响,看实际与上次的一样,也能分析出来。
主要是分析第2和第3个数据域是否紧密链接的。OBJECT_ACTIVE_PRE赋值0b00001111,NUMBER_ACTIVE赋值0b00000101,其他变量都是0,看到WORD数值0b1011111000000。分析WORD数据,可以看到这款MCU还是小端格式(高位数据在高端,低位数据在低端,这里不对大小端进行讨论),断开数据变成(0)10111 11000000,正好是0101+1111,OBJECT_ACTIVE_PRE数据域,跨越了两个字节,并不是刚开始设想的那样。这就印证了上面的紧密链接的结论,也符合数据结果输出。
4、再次实验,分析数据是否紧密链接,看下图和结果
可以看到,RESERVED数据域已经不再属于4个地址空间内了(0x1fff0518 - 0x1fff051b),但是他们整体加起来还是32个位域。这说明数据中间肯定有“空隙”存在了,空隙在哪?看一下NUMBER_STATE,如果紧密的话它应该跟NUMBER_ACTIVE在同一个字节地址上,可是他们并不在一块,“空隙”就存在这里。
这两个结构体有什么不一样?数据类型不一致,一个是uint_32,一个是uint_8。
综上所述:数据类型影响的是编译器在分配物理空间时的大小单位,uint_32 是以 4个字节为单位,而后面的位域则是指在已经分配好的物理空间内部再紧凑的方式分配数据位,当物理空间不能满足位域时,那么系统就再次以一定大小单位进行物理空间分配,这个单位就是上面提到的 uint_8 或者 uint_32。
举例:上面 uint_32 时,这些位域不管是不是在一个字节地址上,如果能够紧凑的分配在一个4字节空间大小上,就直接紧凑分配。如果不能则继续分配(总空间超过4字节),则再次以4字节空间分配,并把新的位域建立在新的地址空间上(条目1上的就是)。当 uint_8 时,很明显如果位域不能紧凑的放在一个字节空间上,那么就从新分配新的1字节空间大小,道理是一样的。
5、结构体组合、共用体组合是否影响上述结论
可以看到,系统并没有因为位结构体上面有uint_4的4字节变量或者共用体类型,就改变分配策略把位域都挤到4字节之内,看来他们是没有什么实质性联系的。这里把uint_32改成uint_8,或者把位结构体也替换掉,经我试验证明,都是没有任何影响的。
第九:总结:
1、在操作位结构体时,要关注变量的位域是否在一个变量类型(uint_32或者uint_8)上,判断占用空间大小。
2、除了位域,还要关注变量定义类型,因为编译器空间分配始终是按类型分配的,位域只是指出了有效位(小于类型占用空间),而且如果位域大于类型空间,编译器直接报错(如 uint_8 test :15,可自行实验)。
3、这两个因素都影响变量占用空间大小,具体可以结合调试窗口,通过地址分配分析判断。
4、最重要的一点:上面的所有结果,都是基于我自己的 CodeWarrior10.2 和MQX3.8 分析出来的,不同的编译环境和操作系统,都可能会有不同的结果;而且即便是环境相同,编译器的配置和优化选项都有可能影响系统处理结果。结论并不重要,主要想告诉大家这一块隐藏陷阱,在以后处理类似问题时,要注意分析避让并掌握方法。