当前位置:首页 > > 大橙子疯嵌入式


1、前言

相信不少朋友在编程的时候,都有用到过sizeof()关键词得到结构体的内存大小;在开发系统参数保存功能的时候,通过定义一个结构体,将所有的系统参数都作为结构体成员变量,然后保存。

结构体保存的数据都是二进制数据,非常适合作为 MCU 的参数储存方式,但是这种方式存在一个缺点:扩展性不高
这种缺点一般通过结构体成员空间预留的方式能得到解决。

2、结构体预留

通常通过预留的方式进行后期的参数扩展,如:

typedef struct { uint8_t testParam; uint8_t testParam2; uint8_t reserve[6]; // 预留 } TestParam_t; /* 某模块参数 */ typedef struct { uint8_t testParam; uint8_t testParam2; uint8_t reserve1[10]; // 预留 TestParam_t tTestParam; uint16_t testParam3; uint8_t reserve2[10]; // 预留 } SystemParam_t; /* 系统参数 */ 

这种方式在预留位置扩展新的成员变量时,都要保证结构体大小不变,且内部的成员变量偏移位置不变,因为在增加新的成员变量时由于结构体填充的原因容易导致结构体发生填充,从而不小心改变了结构体大小,甚至改变了其他成员的偏移位置。

后果:在系统升级后读取参数时就会因为结构体大小和升级前的不一致或者部分变量内存偏移改变引发系统异常。
所以在这种情况下,每次增加新的变量后都要仔细算一下结构大小有没有改变,甚至推算里面的结构体成员相对偏移位置有没有变化。

每次新增参数,手动计算和校验 99% 可以检查出来,但是人总有粗心的时候(加班多了,状态不好…),且结构体存在填充,一不留神就以为没问题,提交代码,出版本(测试不一定能发现),给客户,升级后异常,客户投诉、扣工资(难啊….)

遇到这种问题后:难道编译器就没有在编译的时候检查这个大小或者结构体成员的偏移吗,每次手动计算校验好麻烦啊,一不留神还容易算错

哎,你别说,这种还真可以实现···

3、结构体大小检查

利用宏定义在编译期间自动检查结构体大小,在编译的时候就能将错误暴露出来,宏定义如下:

/**
 * @brief 检查结构体大小是否符合,在编译时会进行检查
 *
 * @param type 结构体类型
 * @param size 结构体检查大小
 */ #define TYPE_CHECK_SIZE(type, size) \
extern int sizeof_##type##_is_error [!!(sizeof(type)==(size_t)(size)) - 1] 

使用方式:

typedef struct { uint8_t testParam; uint8_t testParam2; uint8_t reserve[6]; // 预留 } TestParam_t; /* 某模块参数 */ TYPE_CHECK_SIZE(TestParam_t, 8); // 检查结构体的大小是否符合预期 

在TestParam_t中增加一个变量,假设不小心预留大小写错了:

typedef struct { uint8_t testParam; uint8_t testParam2; uint16_t testParam3; uint8_t reserve[5]; // 预留 } TestParam_t; /* 某模块参数 */ TYPE_CHECK_SIZE(TestParam_t, 8); // 检查结构体的大小是否符合预期 

编译器报错内容(通过sizeof_TestParam_t_is_error就能定位是哪个结构体):

4、结构体成员相对偏移检查

利用宏定义在编译期间自动检查结构体中的成员变量偏移地址,在编译的时候就能将错误暴露出来,宏定义如下:

/**
 * @brief 检查结构体成员偏移位置是否符合, 在编译时会进行检查
 * @param type 结构体类型
 * @param member 结构体成员
 * @param value 成员偏移
 */ #define TYPE_MEMBER_CHECK_OFFSET(type, member, value) \
 extern int offset_of_##member##_in_##type##_is_error \
 [!!(__builtin_offsetof(type, member)==((size_t)(value))) - 1] 

使用方式:

typedef struct { uint8_t testParam; uint8_t testParam2; uint8_t reserve[6]; // 预留 } TestParam_t; /* 某模块参数 */ TYPE_MEMBER_CHECK_OFFSET(TestParam_t, testParam2, 1); typedef struct { uint8_t testParam; uint8_t testParam2; uint8_t reserve1[10]; // 预留 TestParam_t tTestParam; uint16_t testParam3; uint8_t reserve2[10]; // 预留 } SystemParam_t; /* 系统参数 */ TYPE_MEMBER_CHECK_OFFSET(SystemParam_t, tTestParam, 12);

在SystemParam_t中尝试修改成员变量tTestParam的偏移位置检查:

typedef struct { uint8_t testParam; uint8_t testParam2; uint8_t reserve1[10]; // 预留 TestParam_t tTestParam; uint16_t testParam3; uint8_t reserve2[10]; // 预留 } SystemParam_t; /* 系统参数 */ TYPE_MEMBER_CHECK_OFFSET(SystemParam_t, tTestParam, 13);

编译时则报错:(通过offset_of_testParam2_in_TestParam_t_is_error就能定位是哪个结构体的哪个成员变量偏移位置不对了):

5、总结

上述宏定义检查方式是通过声明一个数组,检查正确则是数组[0],否则就是数组[-1],合理地利用编译器规则(前提是编译器支持定义数组[0])来检查结构体的大小和成员变量的偏移。

这个写法而且只占用文本大小,编译后不占内存!!!


本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
关闭