这个C语言技巧,刷新了我对结构体的认知

最专业的白癜风医院 https://yyk.39.net/bj/zhuanke/89ac7.html
年了,想必已经不会有人对嵌入式开发中“数据结构(DataStructure)”的作用产生疑问了吧?无论你是否心存疑惑,本文都将给你一个完全不同的视角。每每说起数据结构,很多人脑海里复现的一定是以下内容:

●看似简单,但实际操作起来很容易出错的链表;

●每天都挂在嘴边的队列;

●程序跑飞的第一嫌疑人(没有之一):栈——其实平时根本没有自己用过;

●稀里糊涂揉在一起说的“堆栈”——其实脑海里想的只是malloc,其实跟栈(Stack)一毛钱关系都没有;

●几乎从未触碰过的树(Tree)和图(Graph)。

数据结构其实不是一个高大上的名词,它意外的非常朴实——你也许每天都在用。作为一个新坑,我将在系列文章中为大家分享很多嵌入式开发中很多“非常”而又“好用”的数据结构。你不必学过所谓的“关系数据库”也可以理解“表格(Table)”这种数据结构的本质含义。在C语言环境中,表格的本质就是结构体数组,即:由结构体组成的数组。这里:

●表格由一条条的“记录(Record)”构成,有时候也被称为“条目(Item)”;

●结构体负责定义每条“记录”中内容的构成;

●一个表格就是一个结构体数组。

在嵌入式系统中,表格具有以下特点:●是一个常量数组,以const来修饰,一般保存在ROM(比如Flash)中;●在编译时刻初始化;●在运行时刻使用;●以极其紧凑的形式保存数据;●能够以“数组+下标”的形式加以访问。如果一个需求能够接受上述的特点,或者本身就具有上述特点,亦或部分内容经过改造后可以接受上述特点。那么,就可以使用表格来保存数据了。一个典型的例子就是:交互菜单。

很容易看到,每一级菜单本质上都“可以”是一个表格。

虽然在很多UI设计工具中(比如LVGL),菜单的内容是在运行时刻动态生成的(用链表来实现),但在嵌入式系统中,动态生成表格本身并不是一个“必须使用”的特性。

相反,由于产品很多时候功能固定——菜单的内容也是固定的,因此完全没有必要在运行时刻进行动态生成——这就满足了表格的“在编译时刻初始化”的要求。

采用表格的形式来保存菜单,就获得了在ROM中保存数据、减少RAM消耗的的优势。同时,数组的访问形式又进一步简化了用户代码。

另外一个常见用到表格的例子是消息地图(MessageMap),它在通信协议栈解析类的应用中非常常见,在很多结构紧凑功能复杂的bootloader中也充当着重要的角色。

如果你较真起来,菜单也不过消息地图的一种。表格不是实现消息地图的唯一方式,但却是最简单、最常用、数据存储密度最高的形式。在后续的例子中,我们就以“消息地图”为例,深入聊聊表格的使用和优化。

一般来说,表格由两部分构成:

●记录(又叫条目)

●记录的容器

因此,表格的定义也分为两个部分:

●定义记录/条目的结构体类型

●定义容器的类型

记录的定义一般格式如下:

typedefstruct表格名称_item_t表格名称_item_t;struct表格名称_item_t{//每条记录中的内容};

这里,第一行的typedef所在行的作用是“前置声明”;struct所在行的作用是定义结构体的实际内容。虽然我们完全可以将“前置声明”和“结构体定义”合二为一,写作:

typedefstruct表格名称_item_t{//每条记录中的内容}表格名称_item_t;

但基于以下两大原因,我们还是推荐大家坚持第一种写法:

●由于“前置声明”的存在,我们可以在结构体定义中直接使用“表格名称_item_t”来定义指针;

●由于“前置声明”的存在,多个不同类型的记录之间可以“交叉”定义指针。

以消息地图为例,一个常见的记录结构体定义如下:

typedefstructmsg_item_tmsg_item_t;structmsg_item_t{uint8_tchID;//!指令uint8_tchAccess;//!访问权限检测uint16_thwValidDataSize;//!数据长度要求bool(*fnHandler)(msg_item_t*ptMSG,void*pData,uint_fast16_thwSize);};

在这个例子中,我们脑补了一个通信指令系统,当我们通过通信前端进行数据帧解析后,获得了以下的内容:

●8bit的指令

●用户传来的不定长数据

为了方便指令解析,我们也需要有针对性的来设计每一条指令的内容,因此,我们加入了chID来存储指令码;并加入了函数指针fnHandler来为当前指令绑定一个处理函数;考虑到每条指令所需的最小有效数据长度是已知的。因此,我们通过hwValidDataSize来记录这一信息,以便进行信息检索时快速的做出判断。具体如何使用,我们后面再说。对表格来说,容器是所有记录的容身之所,可以简单,但不可以缺席。最简单的容器就是数组,例如:

constmsg_item_tc_tMSGTable[20];

这里,msg_item_t类型的数组就是表格的容器,而且我们手动规定了数组中元素的个数。

实践中,我们通常不会像这样手动的“限定”表格中元素的个数,而是直接“偷懒”——埋头初始化数组,然后让编译器替我们去数数——根据我们初始化元素的个数来确定数组的元素数量,例如:

constmsg_item_tc_tMSGTable[]={[0]={.chID=0,.fnHandler=NULL,},[1]={...},...};

上述写法是C99语法,不熟悉的小伙伴可以再去翻翻语法书哦。说句题外话,年了,连顽固不化的Linux都拥抱C11了,不要再抱着C89规范不放了,起码用个C99没问题的。

上面写法的好处主要是方便我们偷懒,减少不必要的“数数”过程。那么,我们要如何知道一个表格中数组究竟有多少个元素呢?

别慌,我们有sizeof():

#ifndefdimof#dimof(__array)(sizeof(__array)/sizeof(__array[0]))#endif

这个语法糖dimof()可不是我发明的,不信你问Linux。它的原理很简单,当我们把数组名称传给dimof()时,它会:

●通过sizeof(数组)来获取整个目标数组的字节尺寸;

●通过sizeof(数组[0])来获取数组第一个元素的字节尺寸,也就是数组元素的尺寸;

●通过除法获取数组中元素的个数。

由于表格的本质是结构体数组,因此,针对表格最常见的操作就是遍历(搜索)了。还以前面消息地图为例子:

staticvolatileuint8_ts_chCurrentAccessPermission;/*!\brief搜索消息地图,并执行对应的处理程序*!\retvalfalse消息不存在或者消息处理函数觉得内容无效*!\retvaltrue消息得到了正确的处理*/boolsearch_msgmap(uint_fast8_tchID,void*pData,uint_fast16_thwSize){for(intn=0;ndimof(c_tMSGTable);n++){msg_item_t*ptItem=c_tMSGTable[n];if(chID!=ptItem-chID){continue;}if(!(ptItem-chAccesss_chCurrentAccessPermission)){continue;//!当前的访问属性没有一个符合要求}if(hwSizeptItem-hwSize){continue;//!数据太小了}if(NULL==ptItem-fnHandler){continue;//!无效的指令?(不应该发生)}//!调用消息处理函数returnptItem-fnHandler(ptItem,pData,hwSize);}returnfalse;//!没找到对应的消息}

别看这个函数“很有料”的样子,其本质其实特别简单:

●通过for循环依次访问表格的中的每一个条目;

●通过dimof来确定for循环的次数;

●找到条目后做一系列所谓的“把关工作”,比如检查权限、检查数据有效性之类的,这些部分都是具体项目具体实现的,并非访问表格所必须的,放在这里只是一种参考;

●如果条目符合要求,就通过函数指针执行对应的处理程序。

其实上述代码隐藏了一个特性:就是这个例子中的消息地图中允许出现chID相同的消息的——这里的技巧是:对同一个chID值的消息,我们可以针对不同的访问权限(chAccess值)来提供不同的处理函数。比如,通信系统中,我们可以设计多种权限和模式,比如:只读模式、只写模式、安全模式等。不同模式对应不同的chAccess值。这样,对哪怕同样的指令,我们也可以根据当前模式的不同提供不同的处理函数——这只是一种思路,供大家参考。前面的例子为我们展示表格使用的大体细节,对很多嵌入式应用场景来说,已经完全够用了。但爱思考的小伙伴一定已经发现了问题:如果我的系统中有多个消息地图(每个消息地图中消息数量是不同的),我改怎么复用代码呢?

为了照顾还一脸懵逼的小伙伴,我把这个问题给大家翻译翻译:

●系统中会有多个消息地图(多个表格),这意味着系统中会有多个表格的数组;●前面的消息地图访问函数search_msgmap()跟某一个数组(也就是c_tMSGTable)绑定死了:

(1)只会遍历这一个固定的数组c_tMSGTable

(2)for循环的次数也只针对数组c_tMSGTable

简而言之,search_msgmap()现在跟某一个消息地图(数组)绑定死了,如果要让它支持其它的消息地图(其它数组),就必须想办法将其与特定的数组解耦,换句话说,在使用search_msgmap()的时候,要提供目标的消息地图的指针,以及消息地图中元素的个数。

一个头疼医头脚疼医脚的修改方案呼之欲出:

boolsearch_msgmap(msg_item_t*ptMSGTable,uint_fast16_thwCount,uint_fast8_tchID,void*pData,uint_fast16_thwSize){for(intn=0;nhwCount;n++){msg_item_t*ptItem=ptMSGTable[n];if(chID!=ptItem-chID){continue;}...//!调用消息处理函数returnptItem-fnHandler(ptItem,pData,hwSize);}returnfalse;//!没找到对应的消息}假设我们有多个消息地图,对应不同的工作模式:

constmsg_item_tc_tMSGTableUserMode[]={...};constmsg_item_tc_tMSGTableSetupMode[]={...};constmsg_item_tc_tMSGTableDebugMode[]={...};constmsg_item_tc_tMSGTableFactoryMode[]={...};

在使用的时候,可以这样:

typedefenum{USER_MODE=0,//!普通的用户模式SETUP_MODE,//!出厂后的安装模式DEBUG_MODE,//!工程师专用的调试模式FACTORY_MODE,//!最高权限的工厂模式}


转载请注明:http://www.aierlanlan.com/tzrz/192.html