
【说在前面的话】
看似简单,但实际操作起来很容易出错的链表;
每天都挂在嘴边的队列;
程序跑飞的第一嫌疑人(没有之一):栈——其实平时根本没有自己用过;
稀里糊涂揉在一起说的“堆栈”——其实脑海里想的只是malloc,其实跟栈(Stack)一毛钱关系都没有
几乎从未触碰过的树(Tree)和图(Graph)

【人人都可以学会的“表格”】
表格由一条条的“记录(Record)”构成,有时候也被称为“条目(Item)”
结构体负责定义每条“记录”中内容的构成
一个表格就是一个结构体数组
是一个常量数组,以const来修饰,一般保存在ROM(比如Flash)中
在编译时刻初始化
在运行时刻使用
以极其紧凑的形式保存数据
能够以“数组+下标”的形式加以访问

记录(又叫条目)
记录的容器
定义记录/条目的结构体类型
定义容器的类型
typedef struct <表格名称>_item_t <表格名称>_item_t;struct <表格名称>_item_t {// 每条记录中的内容};
typedef struct <表格名称>_item_t {// 每条记录中的内容} <表格名称>_item_t;
由于“前置声明”的存在,我们可以在结构体定义中直接使用“<表格名称>_item_t” 来定义指针;
由于“前置声明”的存在,多个不同类型的记录之间可以“交叉”定义指针。
以消息地图为例,一个常见的记录结构体定义如下:
typedef struct msg_item_t msg_item_t;struct msg_item_t {uint8_t chID; //!< 指令uint8_t chAccess; //!< 访问权限检测uint16_t hwValidDataSize; //!< 数据长度要求bool (*fnHandler)(msg_item_t *ptMSG,void *pData,uint_fast16_t hwSize);};
8bit的指令
用户传来的不定长数据
const msg_item_t c_tMSGTable[20];const msg_item_t c_tMSGTable[] = {[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...};
上面写法的好处主要是方便我们偷懒,减少不必要的“数数”过程。那么,我们要如何知道一个表格中数组究竟有多少个元素呢?别慌,我们有 sizeof():
通过 sizeof(<数组>) 来获取整个目标数组的字节尺寸;
通过 sizeof(<数组>[0]) 来获取数组第一个元素的字节尺寸——也就是数组元素的尺寸;
通过除法获取数组中元素的个数。
【表格的访问(遍历)】
由于表格的本质是结构体数组,因此,针对表格最常见的操作就是遍历(搜索)了。还以前面消息地图为例子:
static volatile uint8_t s_chCurrentAccessPermission;/*! \brief 搜索消息地图,并执行对应的处理程序*! \retval false 消息不存在或者消息处理函数觉得内容无效*! \retval true 消息得到了正确的处理*/bool search_msgmap(uint_fast8_t chID,void *pData,uint_fast16_t hwSize){for (int n = 0; n < dimof(c_tMSGTable); n++) {msg_item_t *ptItem = &c_tMSGTable[n];if (chID != ptItem->chID) {continue;}if (!(ptItem->chAccess & s_chCurrentAccessPermission)) {continue; //!< 当前的访问属性没有一个符合要求}if (hwSize < ptItem->hwSize) {continue; //!< 数据太小了}if (NULL == ptItem->fnHandler) {continue; //!< 无效的指令?(不应该发生)}//! 调用消息处理函数return ptItem->fnHandler(ptItem, pData, hwSize);}return false; //!< 没找到对应的消息}
通过for循环依次访问表格的中的每一个条目;
通过 dimof 来确定 for 循环的次数
找到条目后做一系列所谓的“把关工作”,比如检查权限啊,检查数据有效性啊之类的——这些部分都是具体项目具体实现的,并非访问表格所必须的——放在这里只是一种参考。
如果条目符合要求,就通过函数指针执行对应的处理程序。
前面的例子为我们展示表格使用的大体细节,对很多嵌入式应用场景来说,已经完全够用了。但爱思考的小伙伴一定已经发现了问题:
如果我的系统中有多个消息地图(每个消息地图中消息数量是不同的),我改怎么复用代码呢?

为了照顾还一脸懵逼的小伙伴,我把这个问题给大家翻译翻译:
系统中会有多个消息地图(多个表格),这意味着,系统中会有多个表格的数组; 前面的消息地图访问函数 search_msgmap() 跟某一个数组(也就是c_tMSGTable)绑定死了: 只会遍历这一个固定的数组 c_tMSGTable; for 循环的次数也只针对数组 c_tMSGTable;
一个头疼医头脚疼医脚的修改方案呼之欲出:
bool search_msgmap(msg_item_t *ptMSGTable,uint_fast16_t hwCount,uint_fast8_t chID,void *pData,uint_fast16_t hwSize){for (int n = 0; n < hwCount; n++) {msg_item_t *ptItem = &ptMSGTable[n];if (chID != ptItem->chID) {continue;}...//! 调用消息处理函数return ptItem->fnHandler(ptItem, pData, hwSize);}return false; //!< 没找到对应的消息}
假设我们有多个消息地图,对应不同的工作模式:
const msg_item_t c_tMSGTableUserMode[] = {...};const msg_item_t c_tMSGTableSetupMode[] = {...};const msg_item_t c_tMSGTableDebugMode[] = {...};const msg_item_t c_tMSGTableFactoryMode[] = {...};
typedef enum {USER_MODE = 0, //!< 普通的用户模式SETUP_MODE, //!< 出厂后的安装模式DEBUG_MODE, //!< 工程师专用的调试模式FACTORY_MODE, //!< 最高权限的工厂模式} comm_mode_t;bool frame_process_backend(comm_mode_t tWorkMode,uint_fast8_t chID,void *pData,uint_fast16_t hwSize){bool bHandled = false;switch (tWorkMode) {case USER_MODE:bHandled = search_msgmap(c_tMSGTableUserMode,dimof(c_tMSGTableUserMode),chID,pData,hwSize);break;case SETUP_MODE:bHandled = search_msgmap(c_tMSGTableSetupMode,dimof(c_tMSGTableUserMode),chID,pData,hwSize);break;...}return bHandled;}

前面我们说过,表格的定义分两个部分:
定义记录/条目的结构体类型
定义容器的类型
其中,关于容器的定义,我们说过,数组是容器的最简单形式。那么容器定义的完全体是怎样的呢?

“还是结构体”!
是的,表格条目的本质是结构体,表格容器的本质也是一个结构体:
typedef struct <表格名称>_item_t <表格名称>_item_t;struct <表格名称>_item_t {// 每条记录中的内容};typedef struct <表格名称>_t <表格名称>_t;struct <表格名称>_t {uint16_t hwItemSize;uint16_t hwCount;<表格名称>_item_t *ptItems;};
ptItems:一个指针,指向条目数组;
hwCount:条目数组的元素个数
hwItemSize:每个条目的尺寸
这个hwItemSize其实是来凑数的,因为32位系统中指针4字节对齐的缘故,2字节的hwCount横竖会产生2字节的气泡。不理解这一点的小伙伴,可以参考文章《漫谈C变量——对齐(3)》
typedef struct msg_item_t msg_item_t;struct msg_item_t {uint8_t chID; //!< 指令uint8_t chAccess; //!< 访问权限检测uint16_t hwValidDataSize; //!< 数据长度要求bool (*fnHandler)(msg_item_t *ptMSG,void *pData,uint_fast16_t hwSize);};typedef struct msgmap_t msgmap_t;struct msgmap_t {uint16_t hwItemSize;uint16_t hwCount;msg_item_t *ptItems;};const msg_item_t c_tMSGTableUserMode[] = {...};const msgmap_t c_tMSGMapUserMode = {.hwItemSize = sizeof(msg_item_t),.hwCount = dimof(c_tMSGTableUserMode),.ptItems = c_tMSGTableUserMode,};
bool search_msgmap(msgmap_t *ptMSGMap,uint_fast8_t chID,void *pData,uint_fast16_t hwSize){for (int n = 0; n < ptMSGMap->hwCount; n++) {msg_item_t *ptItem = &(ptMSGMap->ptItems[n]);if (chID != ptItem->chID) {continue;}...//! 调用消息处理函数return ptItem->fnHandler(ptItem, pData, hwSize);}return false; //!< 没找到对应的消息}

“是的……是稍微优雅一点……然后呢?”
“就这!?就这?!”
别急,下面才是见证奇迹的时刻。
在前面的例子中,我们注意到表格的初始化是分两部分进行的:
const msg_item_t c_tMSGTableUserMode[] = {[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...};const msgmap_t c_tMSGMapUserMode = {.hwItemSize = sizeof(msg_item_t),.hwCount = dimof(c_tMSGTableUserMode),.ptItems = c_tMSGTableUserMode,};
那么,我们可不可以把它们合二为一呢?这样:
所有的初始化写在一起; 避免给完全用不到的条目数组起名字:
要做到这一点,我们可以使用一个类似“匿名数组”的功能:
const msgmap_t c_tMSGMapUserMode = {.hwItemSize = sizeof(msg_item_t),.hwCount = dimof(c_tMSGTableUserMode),.ptItems = const msg_item_t c_tMSGTableUserMode[] = {[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...},};
const msgmap_t c_tMSGMapUserMode = {.hwItemSize = sizeof(msg_item_t),.hwCount = dimof(c_tMSGTableUserMode),.ptItems = (msg_item_t []){[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...},};

其实,这不是什么“黑魔法”,而是一个广为使用的GNU扩展,被称为“复合式描述(Compound literal)”,本质上就是一种以“省略”数组或结构体名称的方式来初始化数组或结构体的语法结构。具体语法介绍,小伙伴们可以参考这篇文章《C语言语法中匿名的最高境界》。

眼尖的小伙伴也许已经发现了问题:既然我们省略了变量名,那么如何通过 dimof() 来获取数组元素的个数呢?
少侠好眼力!
解决方法不仅有,而且简单粗暴:
const msgmap_t c_tMSGMapUserMode = {.hwItemSize = sizeof(msg_item_t),.hwCount = dimof((msg_item_t []){[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...}),.ptItems = (msg_item_t []){[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...},};

所以说?……
为了优雅的初始化……
我们要把同样的内容写两次?!!

手写的确挺愚蠢,但宏可以啊!
.ptItems = (__item_type []) { \__VA_ARGS__ \}, \.hwCount = sizeof((__item_type []) { __VA_ARGS__ }) \/ sizeof(__item_type), \.hwItemSize = sizeof(__item_type)__impl_table(__item_type, __VA_ARGS__)
const msgmap_t c_tMSGMapUserMode = {impl_table(msg_item_t,[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...),};
const msg_item_t c_tMSGTableUserMode[] = {...};const msg_item_t c_tMSGTableSetupMode[] = {...};const msg_item_t c_tMSGTableDebugMode[] = {...};const msg_item_t c_tMSGTableFactoryMode[] = {...};
现在当然就要改为如下的形式了:
const msgmap_t c_tMSGMapUserMode = {impl_table(msg_item_t,...),};const msgmap_t c_tMSGMapSetupMode = {impl_table(msg_item_t,...),};const msgmap_t c_tMSGMapDebugMode = {impl_table(msg_item_t,...),};const msgmap_t c_tMSGMapFactoryMode = {impl_table(msg_item_t,...),};
但……它们不都是 msgmap_t 类型的么?为啥不做一个数组呢?
typedef enum {USER_MODE = 0, //!< 普通的用户模式SETUP_MODE, //!< 出厂后的安装模式DEBUG_MODE, //!< 工程师专用的调试模式FACTORY_MODE, //!< 最高权限的工厂模式} comm_mode_t;const msgmap_t c_tMSGMap[] = {[USER_MODE] = {impl_table(msg_item_t,...),},[SETUP_MODE] = {impl_table(msg_item_t,...),},[DEBUG_MODE] = {impl_table(msg_item_t,...),},[FACTORY_MODE] = {impl_table(msg_item_t,...),},};
是不是有点意思了?再进一步,我们完全可以做一个新的表格,表格的元素就是 msgmap_t 呀?
typedef struct cmd_modes_t cmd_modes_t;struct cmd_modes_t {uint16_t hwItemSize;uint16_t hwCount;msgmap_t *ptItems;};
然后就可以开始套娃咯:
const cmd_modes_t c_tCMDModes = {impl_table(msgmap_t,[USER_MODE] = {impl_table(msg_item_t,[0] = {.chID = 0,.fnHandler = NULL,},[1] = {...},...),},[SETUP_MODE] = {impl_table(msg_item_t,...),},[DEBUG_MODE] = {impl_table(msg_item_t,...),},[FACTORY_MODE] = {impl_table(msg_item_t,...),},),};

【差异化……】
extern const cmd_modes_t c_tCMDModes;bool frame_process_backend(comm_mode_t tWorkMode,uint_fast8_t chID,void *pData,uint_fast16_t hwSize){bool bHandled = false;if (tWorkMode > FACTORY_MODE) {return false;}return search_msgmap( &(c_tCMDModes.ptItems[tWorkMode]),chID,pData,hwSize);}
是不是特别优雅?

在结构体内增加更多的成员——为表格添加更多的信息
加入更多的函数指针(用OOPC的概念来说就是加入更多的“方法”)
typedef struct msgmap_t msgmap_t;struct msgmap_t {uint16_t hwItemSize;uint16_t hwCount;msg_item_t *ptItems;bool (*fnHandler)(msgmap_t *ptMSGMap,uint_fast8_t chID,void *pData,uint_fast16_t hwSize);};
externbool msgmap_user_mode_handler(msgmap_t *ptMSGMap,uint_fast8_t chID,void *pData,uint_fast16_t hwSize);externbool msgmap_debug_mode_handler(msgmap_t *ptMSGMap,uint_fast8_t chID,void *pData,uint_fast16_t hwSize);const cmd_modes_t c_tCMDModes = {impl_table(msgmap_t,[USER_MODE] = {impl_table(msg_item_t,...),.fnHandler = &msgmap_user_mode_handler,},[SETUP_MODE] = {impl_table(msg_item_t,...),.fnHandler = NULL; //!< 使用默认的处理函数},[DEBUG_MODE] = {impl_table(msg_item_t,...),.fnHandler = &msgmap_debug_mode_handler,},[FACTORY_MODE] = {impl_table(msg_item_t,...),//.fnHandler = NULL 什么都不写,就是NULL(0)},),};
bool frame_process_backend(comm_mode_t tWorkMode,uint_fast8_t chID,void *pData,uint_fast16_t hwSize){bool bHandled = false;msgmap_t *ptMSGMap = c_tCMDModes.ptItems[tWorkMode];if (tWorkMode > FACTORY_MODE) {return false;}//! 调用每个消息地图自己的处理程序if (NULL != ptMSGMap->fnHandler) {return ptMSGMap->fnHandler(ptMSGMap,chID,pData,hwSize);}//! 默认的消息地图处理程序return search_msgmap( ptMSGMap,chID,pData,hwSize);}
版权声明:本文来源网络,免费传达知识,版权归原作者所有。如涉及作品版权问题,请联系我进行删除。
‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧ END ‧‧‧‧‧‧‧‧‧‧‧‧‧‧‧
关注我的微信公众号,回复“加群”按规则加入技术交流群。
点击“阅读原文”查看更多分享,欢迎点分享、收藏、点赞、在看