有时我会遇到一个带有状态机的程序,会被频繁修改的问题

这时候就需要表驱动状态机了,

学会表驱动状态机,编程思维 就 从频繁改核心代码,变成了 数据驱动编程

/* 表驱动状态机的学习
    个人理解 :  
    表驱动状态机,添加新功能就是添加新函数,然后在表里面写好切换调用关系就行了
    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;  // 清除事件 来实现清理事件

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐