1、概念
所谓表驱动法(Table-Driven Approach)简而言之就是用查表的方法获取数据。此处的“表”通常为数组,但可视为数据库的一种体现。根据字典中的部首检字表查找读音未知的汉字就是典型的表驱动法,即以每个字的字形为依据,计算出一个索引值,并映射到对应的页数。相比一页一页地顺序翻字典查字,部首检字法效率极高。具体到编程方面,在数据不多时可用逻辑判断语句(if…else或switch…case)来获取值;但随着数据的增多,逻辑语句会越来越长,此时表驱动法的优势就开始显现。
2、简单示例
上面讲概念总是枯燥的,我们简单写一个C语言的例子。下面例子功能:传入不同的数字打印不同字符串。
使用if…else逐级判断的写法如下
void fun(int day) { if (day == 1) { printf("Monday\n"); } else if (day == 2) { printf("Tuesday\n"); } else if (day == 3) { printf("Wednesday\n"); } else if (day == 4) { printf("Thursday\n"); } else if (day == 5) { printf("Friday\n"); } else if (day == 6) { printf("Saturday\n"); } else if (day == 7) { printf("Sunday\n"); } }
使用switch…case的方法写
void fun(int day) { switch (day) { case 1: printf("Monday\n"); break; case 2: printf("Tuesday\n"); break; case 3: printf("Wednesday\n"); break; case 4; printf("Thursday\n"); break; case 5: printf("Friday\n"); break; case 6: printf("Saturday\n"); break; case 7:printf("Sunday\n"); break; default: break; } }
使用表驱动法实现
char weekDay[] = {Monday,Tuesday,Wednesday,Thursday,Friday,Saturday,Sunday}; void fun(int day) { printf("%s\n",weekDay[day]); }
看完示例,可能“恍然大悟”,一拍大腿,原来表驱动法就是这么简单啊。是的,它的核心原理就是这个简单,如上面例子一样。
如果上面的例子还没get这种用法的好处,那么再举一个栗子。
统计用户输入的一串数字中每个数字出现的次数。
常规写法
int32_t aDigitCharNum[10] = {0}; /* 输入字符串中各数字字符出现的次数 */ int32_t dwStrLen = strlen(szDigits); int32_t dwStrIdx = 0; for (; dwStrIdx < dwStrLen; dwStrIdx++) { switch (szDigits[dwStrIdx]) { case '1': aDigitCharNum[0]++; break; case '2': aDigitCharNum[1]++; break; //... ... case '9': aDigitCharNum[8]++; break; } }
表驱动法
for(; dwStrIdx < dwStrLen; dwStrIdx++) { aDigitCharNum[szDigits[dwStrIdx] - '0']++; }
偶尔在一些开源项目中看到类似的操作,惊呼“骚操作”,其实他们有规范的叫法:表驱动法。
3、在MCU中应用
在MCU中的应用示例,怎么少的了点灯大师操作呢?首先来点一下流水LED灯吧。
常规写法
void LED_Ctrl(void) { static uint32_t sta = 0; if (0 == sta) { LED1_On(); } else { LED1_Off(); } if (1 == sta) { LED2_On(); } else { LED2_Off(); } /* 两个灯,最大不超过2 */ sta = (sta + 1) % 2; } /* 主函数运行 */ int main(void) { while (1) { LED_Ctrl(); os_delay(200); } }
表驱动法
extern void LED1_On(void); extern void LED1_Off(void); extern void LED2_On(void); extern void LED2_Off(void); /* 把同一个灯的操作封装起来 */ struct tagLEDFuncCB { void (*LedOn)(void); void (*LedOff)(void); }; /* 定义需要操作到的灯的表 */ const static struct tagLEDFuncCB LedOpTable[] = { {LED1_On, LED1_Off}, {LED2_On, LED2_Off}, }; void LED_Ctrl(void) { static uint32_t sta = 0; uint8_t i; for (i = 0; i < sizeof(LedOpTable) / sizeof(LedOpTable[0]); i++) { (sta == i) ? (LedOpTable[i].LED_On()) : (LedOpTable[i].LED_Off()); } /* 跑下个灯 */ sta = (sta + 1) % (sizeof(LedOpTable) / sizeof(LedOpTable[0])); } int main(void) { while (1) { LED_Ctrl(); os_delay(200); } }
这样的代码结构紧凑,因为和结构体结合起来了,方便添加下一个LED灯到流水灯序列中,这其中涉及到函数指针,详细请看《回调函数》,只需要修改LedOpTable如下
const static struct tagLEDFuncCB LedOpTable[] = { {LED1_On, LED1_Off}, {LED2_On, LED2_Off}, {LED3_On, LED3_Off}, };
这年头谁还把流水灯搞的这么花里胡哨的啊,那么就举例在串口解析中的应用,之前的文章推送过《回调函数在命令解析中的应用》,下面只贴一下代码
typedef struct { rt_uint8_t CMD; rt_uint8_t (*callback_func)(rt_uint8_t cmd, rt_uin