1、概念 所謂表驅動法(Table-Driven Approach)簡而言之就是用查表的方法獲取數據。此處的“表”通常為數組,但可視為資料庫的一種體現。根據字典中的部首檢字表查找讀音未知的漢字就是典型的表驅動法,即以每個字的字形為依據,計算出一個索引值,並映射到對應的頁數。相比一頁一頁地順序翻字典查 ...
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_uint8_t *msg, uint8_t len); } _FUNCCALLBACK; _FUNCCALLBACK callback_list[] = { {cmd1, func_callback1}, {cmd2, func_callback2}, {cmd3, func_callback3}, {cmd4, func_callback41}, ... }; void poll_task(rt_uint8_t cmd, rt_uint8_t *msg, uint8_t len) { int cmd_indexmax = sizeof(callback_list) / sizeof(_FUNCCALLBACK); int cmd_index = 0; for (cmd_index = 0; cmd_index < cmd_indexmax; cmd_index++) { if (callback_list[cmd_index].CMD == cmd) { if (callback_list[cmd_index]) { /* 處理邏輯 */ callback_list[cmd_index].callback_func(cmd, msg, len); } } } }
除上述例子,表驅動法在UI界面中也有良好的應用,如下
結構體封裝
typedef enum { stage1 = 0, stage2, stage3, stage4, stage5, stage6, stage7, stage8, stage9, } SCENE; typedef struct { void (*current_operate)(); //當前場景的處理函數 SCENE Index; //當前場景的標簽 SCENE Up; //按下Up鍵跳轉的場景 SCENE Down; //按下Down鍵跳轉的場景 SCENE Right; //按下Left鍵跳轉的場景 SCENE Left; //按下Right鍵跳轉的場景 } STAGE_TAB;
函數映射表
STAGE_TAB stage_tab[] = { //operate Index Up Down Left Right {Stage1_Handler, stage1, stage4, stage7, stage3, stage2}, {Stage2_Handler, stage2, stage5, stage8, stage1, stage3}, {Stage3_Handler, stage3, stage6, stage9, stage2, stage1}, {Stage4_Handler, stage4, stage7, stage1, stage6, stage5}, {Stage5_Handler, stage5, stage8, stage2, stage4, stage6}, {Stage6_Handler, stage6, stage9, stage3, stage5, stage4}, {Stage7_Handler, stage7, stage1, stage4, stage9, stage8}, {Stage8_Handler, stage8, stage2, stage5, stage7, stage9}, {Stage9_Handler, stage9, stage3, stage6, stage8, stage7}, };
定義兩個變數保存當前場景和上一個場景
char current_stage=stage1; char prev_stage=current_stage;
按下Up按鍵 跳轉到指定場景current_stage的值根據映射表改變
current_stage =stage_tab[current_stage].Up;
場景改變後 根據映射表執行相應的函數Handler
if(current_stage!=prev_stage) { stage_tab[current_stage].current_operate(); prev_stage=current_stage; }
這是一個簡單的菜單操作,結合了表驅動法。在MCU中表驅動法有很多很多用處,本文的例子已經過多了,如果在通勤路上用手機看到這裡,已經很難了。關於UI操作,大神figght在github開源了zBitsView倉庫,單片機實現屏幕界面,多層菜單。很牛,很優秀的代碼,有興趣的同學可以學習一下。https://github.com/figght/zBitsView
4、後記
這篇文章我也看到網上一遍表驅動法的後總結的筆記,可能也有很多同學和我一樣,在自己的項目中熟練應用了這種“技巧”,但今天才知道名字:表驅動法。
這篇文章多數都是代碼示例,實在因為表驅動法大家應該都熟練應用了,這篇文章算是總結一下吧。
學習知識,可以像在學校從概念一點點學習,也可以在工作中慢慢積累,然後總結記錄,回歸最初的概念,豐富自己的知識框架。
祝大家變得更強!
點擊查看:C語言進階專輯