上两期的博文《E-Lock 之数据存储》已经讲解了如何把数据存储到 Flash 中,但在实现功能时,肯定会遇到很多问题,今天这期博文我们就来谈谈把数据存入 Flash 中很常见的一个问题——数据结构对齐问题。
- 什么是数据结构对齐
数据结构对齐是代码编译后在内存的布局与使用方式,主要涉及两个问题:数据对齐和数据结构填充。比如一台计算机从内存中读写数据时,它会以一个 word 类型大小(若是 32 位的系统则一个 word 类型大小为 4 字节大小)为依据做数据结构对齐和填充。数据对齐指的是把数据放进一个 word 大小的整数倍的内存地址处,这样做会有助于 CPU 处理内存的方式而提高系统的性能。为了对齐数据,有时会在上一个数据结构的结尾和下一个数据结构的开头插入几个空白字节(padding),这就是数据结构填充。我们在写程序计算数据结构大小时,一般要考虑会不会有上述所说的空白字节的出现。要解决上述问题,我们要先了解下自然对界。
- 自然对界
为了 CPU 高效率存储读取数据,16 位元的 CPU 要求 16 位元以上的数据类型存放的地址就必须要对齐 2 的倍数。以此类推,32 位元的 CPU 要求 32 位元以上的数据类型的存放地址必须对齐 4 的倍数;64 位元的 CPU 则是要求 64 位元以上的数据类型的存放地址必须对齐 8 的倍数。这就是所谓的自然对界。比如以 32 位元的 CPU 来所,uint16_t (大小是 2 byte)必须配置在地址为 2 的倍数上,uint32_t 必须配置在地址为 4 的倍数上,即 uint32_t 的地址的末尾必须是 0x0、0x04、0x8、0xc 的地址上。
由于自然对界的需要,C 编译器一般都依据 CPU 的位元数作为预设的空白字节大小。像16位元的 CPU 用的 C 编译器,预设会在每个结构成员之间加空白字节使其成为 2 byte 的倍数。我们来看下面一个例子做更进一步的解释:
struct {
uint8_t ah;
uint16_t bh;
} st1;
struct {
uint8_t ah;
uint32_t bh;
} st2;
使用 16 位元编译器来编译时,struct st1 和 struct st2 的成员 ah 和 bh 直接会多 1 个 byte 的 padding。如果改用 32 位元的编译器来编译时,结构成员 st1.ah 和 st1.bh 之间会多 1 个 byte 的 padding,但是结构成员 st2.ah 和 st2.bh 之间则会多了 3 个 byte 的 padding。原因则是 uint32_t 在 16 位元的编译器上地址只要对齐到 2 的倍数上,但是在 32 位元的编译器上地址必须对齐 4 的倍数。由于 padding 产生会导致我们的数据存储出现与预期不符的偏差,那么我们在编程中如何避免 padding 的产生呢?请接着往下看。
- 解决 padding 的产生
typedef struct _rtc_datetime
{
uint16_t year; /*!< Range from 1970 to 2099.*/
uint8_t month; /*!< Range from 1 to 12.*/
uint8_t day; /*!< Range from 1 to 31 (depending on month).*/
uint8_t hour; /*!< Range from 0 to 23.*/
uint8_t minute; /*!< Range from 0 to 59.*/
uint8_t second; /*!< Range from 0 to 59.*/
} rtc_datetime_t, *PTime_Type;
上述结构体是一个时间结构体,我们在写应用程序代码是时常需要用到。它的 year 成员是 uint16_t 类型,而该结构体的下一个成员是 uint8_t 类型的 month。在了解了本文上面所述内容后,可知道在用 16 位元的编译器去编译时,成员 year 和 month 之间会产生 1 个 byte 的 padding。那么我们在写程序时如何让编译器不产生这个 padding 呢?
我们可以使用预编译宏 #pragma pack(n) 命令,把该命令放在会产生 padding 的结构体定义的前后,则会设定对齐字节数为 n。通过根据实际情况设置 n 的大小,则可以消除 padding 的产生,从而让结构体的大小与实际相符。如上述结构只需要修改如下即可把 padding 消除,让该结构体在编译后占用实际的大小内存。
#pragma pack(1)
typedef struct _rtc_datetime
{
uint16_t year; /*!< Range from 1970 to 2099.*/
uint8_t month; /*!< Range from 1 to 12.*/
uint8_t day; /*!< Range from 1 to 31 (depending on month).*/
uint8_t hour; /*!< Range from 0 to 23.*/
uint8_t minute; /*!< Range from 0 to 59.*/
uint8_t second; /*!< Range from 0 to 59.*/
} rtc_datetime_t, *PTime_Type;
#pragma pack()
- 总结
本期博文先是讲述了数据结构对齐和自然对界的概念,之后由自然对界引出了数据结构中是如何产生 padding,最后提出了解决 padding 的方法。本期博文不仅让大家对数据结构的空间占用有了更清晰的认识,还让大家在实现数据存储时少了一个拦路虎。
- 参考文献
- CSDN 博客:《#pragma pack(n) 的详细讲解》
链接如下:https://blog.csdn.net/seve0520/article/details/79454229
评论