C语言数据结构笔记8:表驱动状态机 _ 函数指针表
·
有时我会遇到一个带有状态机的程序,会被频繁修改的问题
这时候就需要表驱动状态机了,
学会表驱动状态机,编程思维 就 从频繁改核心代码,变成了 数据驱动编程
/* 表驱动状态机的学习 个人理解 : 表驱动状态机,添加新功能就是添加新函数,然后在表里面写好切换调用关系就行了 handle_event 几乎是写一次就永远不用动 "数据驱动编程"思想 逻辑:写一次,永远不变 数据:随便加,想加多少加多少 */
目录

定义状态和事件:
定义State结构体与 Event结构体
状态State 可以通过 Event的内容指示来进行切换
//定义一些用到的 状态 和 事件 typedef enum { STATE_IDLE, STATE_RUNNING, STATE_PAUSED, STATE_STOPPED, STATE_ANY, // 通配符状态(用于全局规则) STATE_MAX } State; //事件 typedef enum { EVENT_START, EVENT_PAUSE, EVENT_RESUME, EVENT_STOP, EVENT_EMERGENCY_STOP, EVENT_MAX } Event;
定义状态转换表结构体 和 动作函数
// 状态转换表结构 typedef struct { State current_state; Event event; State next_state; void (*action)(); // 状态转换时的动作函数 ()内可以定义传参 } Transition; // 动作函数 定义的传参必须一致 void action_start() { printf(" [动作] Starting...\n"); } void action_pause() { printf(" [动作] Pausing...\n"); } void action_stop() { printf(" [动作] Stopping...\n"); }
写状态转换表:
根据 状态转换表结构体 定义的 结构来写状态转换表:
当前状态 触发事件 状态转换 调用函数
// 状态转换表 //我以后完全可以 粗暴的在这添加 新的状态转换关系 和操作函数 然后直接借此添加新功能 Transition state_transitions[] = { //当前状态 触发事件 状态转换 调用函数 //全局匹配: //任何状态 遇到急停事件 都要停止 //规定"EVENT_EMERGENCY_STOP只有一条全局规则" 全局匹配 就能写在前面 {STATE_ANY, EVENT_EMERGENCY_STOP, STATE_STOPPED, action_stop}, //精确匹配: //当前状态 触发事件 状态转换 调用函数 都一一对应 {STATE_IDLE, EVENT_START, STATE_RUNNING, action_start}, {STATE_RUNNING, EVENT_PAUSE, STATE_PAUSED, action_pause}, {STATE_PAUSED, EVENT_RESUME, STATE_RUNNING, NULL}, {STATE_RUNNING, EVENT_STOP, STATE_STOPPED, action_stop}, {STATE_PAUSED, EVENT_STOP, STATE_STOPPED, action_stop}, {STATE_STOPPED, EVENT_START, STATE_RUNNING, action_start}, // 结束标记 必须最后一条 是 handle_event 的循环结束标志 // STATE_MAX 和 EVENT_MAX 只能出现一次 并在最后一条 // 防止后面 handle_event遍历状态的时候 溢出 {STATE_MAX, EVENT_MAX, STATE_MAX, NULL} };
自环转换
支持在超时事件触发时,使当前状态转移到自身,同时调用为该事件定义的动作函数。
关键点总结: 自环转换完全合法:状态可以转换到自身 常见应用场景: 超时重试 轮询等待 保持状态但执行动作 计数器/定时器重置 注意无限循环:要设置最大重试次数或退出条件 动作函数可以: 重置定时器 更新计数器 记录日志 修改上下文数据这种模式在通信协议、任务调度、错误处理中非常常见!
状态机执行函数:
这个函数的遍历不是以某个下标为结尾,而是以遍历到 STATE_MAX 标志 作为结尾:
这样保证了我以后不论状态转换表新增多少条元素,这个函数都不需要改动
// 状态名称映射(用于打印) const char* state_names[] = {"IDLE","RUNNING", "PAUSED","STOPPED","MAX"}; const char* event_names[] = {"START","PAUSE","RESUME","STOP","MAX"}; // 状态机执行函数 State handle_event(State current, Event event) { printf(" 处理事件: %s\n", event_names[event]); //每次都重新遍历状态的查表,而不是简单遍历状态相同,因为同一个状态可能会因为事件触发不同而切入 不同的状态转换 // 为什么需要遍历?因为同一个当前状态,可能对应多个不同的事件,每个事件会转换到不同的目标状态 // 例如:STATE_RUNNING 状态下: // - 收到 EVENT_PAUSE → 转换到 STATE_PAUSED // - 收到 EVENT_STOP → 转换到 STATE_STOPPED // 所以必须同时匹配"当前状态"和"触发事件"才能找到正确的转换路径 for (int i = 0; state_transitions[i].current_state != STATE_MAX; i++) { if (state_transitions[i].current_state == current && state_transitions[i].event == event) { if (state_transitions[i].action) {state_transitions[i].action();} //调用功能函数 printf(" [转换] %s -> %s\n", state_names[current], state_names[state_transitions[i].next_state]); return state_transitions[i].next_state; } } // 如果没有精确匹配到 EVENT_EMERGENCY_STOP ,查找全局规则(ANY_STATE) // 这里这是专为 EVENT_EMERGENCY_STOP 事件留的 // 但其实是按照 事件重定向 状态的通用遍历 for (int i = 0; state_transitions[i].current_state != STATE_MAX; i++) { if (state_transitions[i].current_state == STATE_ANY && state_transitions[i].event == event) { if (state_transitions[i].action) { state_transitions[i].action(); //调用功能函数 } printf(" [全局规则] 任何状态 -> %s\n", state_names[state_transitions[i].next_state]); return state_transitions[i].next_state; } } printf(" [错误] 无效的状态转换: 当前状态 %s, 事件 %s\n", state_names[current], event_names[event]); return current; }
完整示例程序:
#include <stdio.h> #include <stdlib.h> /* 表驱动状态机的学习 个人理解 : 表驱动状态机,添加新功能就是添加新函数,然后在表里面写好切换调用关系就行了 handle_event 几乎是写一次就永远不用动 "数据驱动编程"思想 逻辑:写一次,永远不变 数据:随便加,想加多少加多少 */ //定义一些用到的 状态 和 事件 typedef enum { STATE_IDLE, STATE_RUNNING, STATE_PAUSED, STATE_STOPPED, STATE_ANY, // 通配符状态(用于全局规则) STATE_MAX } State; //事件 typedef enum { EVENT_START, EVENT_PAUSE, EVENT_RESUME, EVENT_STOP, EVENT_EMERGENCY_STOP, EVENT_MAX } Event; // 状态转换表结构 typedef struct { State current_state; Event event; State next_state; void (*action)(); // 状态转换时的动作函数 ()内可以定义传参 } Transition; // 动作函数 定义的传参必须一致 void action_start() { printf(" [动作] Starting...\n"); } void action_pause() { printf(" [动作] Pausing...\n"); } void action_stop() { printf(" [动作] Stopping...\n"); } // 状态转换表 //我以后完全可以 粗暴的在这添加 新的状态转换关系 和操作函数 然后直接借此添加新功能 Transition state_transitions[] = { //当前状态 触发事件 状态转换 调用函数 //全局匹配: //任何状态 遇到急停事件 都要停止 //规定"EVENT_EMERGENCY_STOP只有一条全局规则" 全局匹配 就能写在前面 {STATE_ANY, EVENT_EMERGENCY_STOP, STATE_STOPPED, action_stop}, //精确匹配: //当前状态 触发事件 状态转换 调用函数 都一一对应 {STATE_IDLE, EVENT_START, STATE_RUNNING, action_start}, {STATE_RUNNING, EVENT_PAUSE, STATE_PAUSED, action_pause}, {STATE_PAUSED, EVENT_RESUME, STATE_RUNNING, NULL}, {STATE_RUNNING, EVENT_STOP, STATE_STOPPED, action_stop}, {STATE_PAUSED, EVENT_STOP, STATE_STOPPED, action_stop}, {STATE_STOPPED, EVENT_START, STATE_RUNNING, action_start}, // 结束标记 必须最后一条 是 handle_event 的循环结束标志 // STATE_MAX 和 EVENT_MAX 只能出现一次 并在最后一条 // 防止后面 handle_event遍历状态的时候 溢出 {STATE_MAX, EVENT_MAX, STATE_MAX, NULL} }; // 状态名称映射(用于打印) const char* state_names[] = {"IDLE","RUNNING", "PAUSED","STOPPED","MAX"}; const char* event_names[] = {"START","PAUSE","RESUME","STOP","MAX"}; // 状态机执行函数 State handle_event(State current, Event event) { printf(" 处理事件: %s\n", event_names[event]); //每次都重新遍历状态的查表,而不是简单遍历状态相同,因为同一个状态可能会因为事件触发不同而切入 不同的状态转换 // 为什么需要遍历?因为同一个当前状态,可能对应多个不同的事件,每个事件会转换到不同的目标状态 // 例如:STATE_RUNNING 状态下: // - 收到 EVENT_PAUSE → 转换到 STATE_PAUSED // - 收到 EVENT_STOP → 转换到 STATE_STOPPED // 所以必须同时匹配"当前状态"和"触发事件"才能找到正确的转换路径 for (int i = 0; state_transitions[i].current_state != STATE_MAX; i++) { if (state_transitions[i].current_state == current && state_transitions[i].event == event) { if (state_transitions[i].action) {state_transitions[i].action();} //调用功能函数 printf(" [转换] %s -> %s\n", state_names[current], state_names[state_transitions[i].next_state]); return state_transitions[i].next_state; } } // 如果没有精确匹配到 EVENT_EMERGENCY_STOP ,查找全局规则(ANY_STATE) // 这里这是专为 EVENT_EMERGENCY_STOP 事件留的 // 但其实是按照 事件重定向 状态的通用遍历 for (int i = 0; state_transitions[i].current_state != STATE_MAX; i++) { if (state_transitions[i].current_state == STATE_ANY && state_transitions[i].event == event) { if (state_transitions[i].action) { state_transitions[i].action(); //调用功能函数 } printf(" [全局规则] 任何状态 -> %s\n", state_names[state_transitions[i].next_state]); return state_transitions[i].next_state; } } printf(" [错误] 无效的状态转换: 当前状态 %s, 事件 %s\n", state_names[current], event_names[event]); return current; } // 获取用户输入 Event get_user_input() { printf("\n请输入命令 (1:START, 2:PAUSE, 3:RESUME, 4:STOP, 5:EVENT_EMERGENCY_STOP 0:退出): "); int choice; scanf("%d", &choice); switch(choice) { case 1: return EVENT_START; case 2: return EVENT_PAUSE; case 3: return EVENT_RESUME; case 4: return EVENT_STOP; case 5: return EVENT_EMERGENCY_STOP; case 0: exit(0); default: printf("无效命令,请重新输入\n"); return EVENT_MAX; // 无效事件 } } int main() { State current_state = STATE_IDLE; Event pending_event = EVENT_MAX; // 待处理事件 printf("=== 状态机演示程序 ===\n"); // 主循环 while(1) { // 获取输入(如果不是自动模式) Event user_event = get_user_input(); if (user_event != EVENT_MAX) { pending_event = user_event; } // 处理待处理事件 if (pending_event != EVENT_MAX) { current_state = handle_event(current_state, pending_event); pending_event = EVENT_MAX; // 清除事件 } } return 0; }
执行效果:
根据键盘输入的event 来切换状态 并打印这个过程:
主函数使用细节:
及时清除事件:
事件EVENT触发了状态EVENT的转换,这个转换完成,并且执行无误后,事件要被清理:
这里可以使用 pending_event = EVENT_MAX; // 清除事件 来实现清理事件
更多推荐


所有评论(0)