3. C语言实现基础:三种状态机实现方式

状态机这东西,说白了就是「当前状态 + 输入事件 → 下一个状态」。我在嵌入式项目里摸爬滚打这么多年,发现很多新手一上来就写嵌套if-else,最后代码改得想哭。今天我就把三种最实用的C语言实现方式掰开揉碎讲清楚。

核心要点:状态机实现没有银弹,不同场景选不同方案。小型控制用switch-case,高性能场景用函数指针表,复杂系统用结构体封装。

3.1 switch-case实现状态机

这是最直观的方式。我刚开始做嵌入式时,第一个状态机就是用switch-case写的——一个简单的LED闪烁控制。你想想看,状态不多、逻辑简单时,这方法简直不要太爽。

// 状态枚举
typedef enum {
    STATE_IDLE,
    STATE_RUNNING,
    STATE_PAUSED,
    STATE_ERROR
} State_t;

// 事件枚举
typedef enum {
    EVT_START,
    EVT_STOP,
    EVT_PAUSE,
    EVT_RESUME,
    EVT_ERROR
} Event_t;

// 状态机处理函数
State_t stateMachine(State_t currentState, Event_t event) {
    State_t nextState = currentState;
    
    switch(currentState) {
        case STATE_IDLE:
            if(event == EVT_START) {
                nextState = STATE_RUNNING;
                printf("系统启动\n");
            }
            break;
            
        case STATE_RUNNING:
            switch(event) {
                case EVT_STOP:
                    nextState = STATE_IDLE;
                    printf("系统停止\n");
                    break;
                case EVT_PAUSE:
                    nextState = STATE_PAUSED;
                    printf("系统暂停\n");
                    break;
                case EVT_ERROR:
                    nextState = STATE_ERROR;
                    printf("发生错误\n");
                    break;
                default:
                    break;
            }
            break;
            
        case STATE_PAUSED:
            if(event == EVT_RESUME) {
                nextState = STATE_RUNNING;
                printf("系统恢复\n");
            } else if(event == EVT_STOP) {
                nextState = STATE_IDLE;
                printf("系统停止\n");
            }
            break;
            
        case STATE_ERROR:
            if(event == EVT_STOP) {
                nextState = STATE_IDLE;
                printf("错误已清除\n");
            }
            break;
    }
    
    return nextState;
}

我的经验:switch-case适合状态数少于10个的场景。我曾经在一个项目里硬塞了30多个状态,结果代码长达500行,调试时眼睛都看花了。后来果断重构,换成了函数指针表。

3.2 函数指针表实现状态机

这种方式就高级一些了。核心思想是用一个二维函数指针数组,把「状态×事件」映射到对应的处理函数。说白了就是查表法,执行效率极高。

// 状态枚举
typedef enum {
    ST_IDLE,
    ST_ACTIVE,
    ST_SLEEP,
    ST_MAX
} State_t;

// 事件枚举
typedef enum {
    EV_TIMEOUT,
    EV_WAKEUP,
    EV_DATA,
    EV_MAX
} Event_t;

// 状态处理函数类型
typedef State_t (*StateHandler_t)(Event_t);

// 各个状态的处理函数
State_t idleHandler(Event_t ev) {
    switch(ev) {
        case EV_DATA:   printf("空闲→激活\n"); return ST_ACTIVE;
        case EV_WAKEUP: printf("空闲→睡眠\n"); return ST_SLEEP;
        default:        return ST_IDLE;
    }
}

State_t activeHandler(Event_t ev) {
    switch(ev) {
        case EV_TIMEOUT: printf("激活→空闲\n"); return ST_IDLE;
        case EV_SLEEP:   printf("激活→睡眠\n"); return ST_SLEEP;
        default:         return ST_ACTIVE;
    }
}

State_t sleepHandler(Event_t ev) {
    switch(ev) {
        case EV_WAKEUP: printf("睡眠→激活\n"); return ST_ACTIVE;
        default:        return ST_SLEEP;
    }
}

// 状态转移表:状态×事件 → 处理函数
StateHandler_t stateTable[ST_MAX][EV_MAX] = {
    /* ST_IDLE:   */ { idleHandler,   idleHandler,   idleHandler },
    /* ST_ACTIVE: */ { activeHandler, activeHandler, activeHandler },
    /* ST_SLEEP:  */ { sleepHandler,  sleepHandler,  sleepHandler }
};

// 状态机执行
State_t runStateMachine(State_t currentState, Event_t event) {
    StateHandler_t handler = stateTable[currentState][event];
    return handler(event);
}

避坑指南:我曾经在函数指针表上栽过跟头——忘记检查事件索引是否越界。如果事件枚举值不是从0开始连续递增,数组索引就会错位。建议加一个断言检查:assert(event < EV_MAX);

函数指针表的好处是:

  • 执行效率高——直接查表跳转,没有if-else链
  • 扩展性好——新增状态或事件,只需加一行表项
  • 代码整洁——每个状态的处理逻辑独立成函数

3.3 结构体封装状态机

这是我最喜欢的方式。把状态机的所有「家当」封装到一个结构体里,包括当前状态、状态表、甚至私有数据。你想想看,一个结构体就是一个完整的状态机实例,多清爽。

// 状态机结构体
typedef struct {
    State_t currentState;           // 当前状态
    StateHandler_t (*table)[EV_MAX]; // 状态转移表
    void *privateData;              // 私有数据(可选)
    uint32_t entryTime;             // 进入当前状态的时间戳
} Fsm_t;

// 初始化状态机
void Fsm_Init(Fsm_t *fsm, State_t initState, 
              StateHandler_t table[][EV_MAX]) {
    fsm->currentState = initState;
    fsm->table = table;
    fsm->privateData = NULL;
    fsm->entryTime = getTick();
}

// 状态机运行
State_t Fsm_Run(Fsm_t *fsm, Event_t event) {
    StateHandler_t handler = fsm->table[fsm->currentState][event];
    State_t nextState = handler(event);
    
    // 状态切换时记录时间
    if(nextState != fsm->currentState) {
        fsm->currentState = nextState;
        fsm->entryTime = getTick();
    }
    
    return nextState;
}

// 使用示例
int main() {
    Fsm_t myFsm;
    StateHandler_t myTable[ST_MAX][EV_MAX] = {
        /* 初始化状态表 */
    };
    
    Fsm_Init(&myFsm, ST_IDLE, myTable);
    
    // 运行状态机
    Fsm_Run(&myFsm, EV_DATA);
    Fsm_Run(&myFsm, EV_TIMEOUT);
    
    return 0;
}

我的习惯:结构体封装时,我喜欢加一个entryTime字段。这在做超时检测时特别有用——比如某个状态停留超过5秒就自动跳转。我在一个物联网网关项目里就用这个特性实现了看门狗功能。

3.4 三种方式对比

特性 switch-case 函数指针表 结构体封装
代码可读性 ⭐⭐⭐ 直观 ⭐⭐⭐⭐ 清晰 ⭐⭐⭐⭐⭐ 优雅
执行效率 ⭐⭐ 有分支开销 ⭐⭐⭐⭐⭐ 直接跳转 ⭐⭐⭐⭐ 略高于查表
扩展性 ⭐ 修改多 ⭐⭐⭐⭐ 加表项 ⭐⭐⭐⭐⭐ 加字段
内存占用 ⭐⭐⭐⭐⭐ 极小 ⭐⭐⭐ 表占空间 ⭐⭐ 结构体开销
适用场景 状态少、逻辑简单 状态中等、性能敏感 复杂系统、多实例

3.5 知识体系总览

下面这张图把三种实现方式的关系和适用场景梳理清楚了。我画图时特意把「结构体封装」放在中间,因为它其实是前两种方式的「升级版」——内部可以用switch-case,也可以用函数指针表。

C语言状态机三种实现方式 状态机核心 当前状态 + 事件 → 下一状态 switch-case 直观·简单·小规模 函数指针表 高效·查表·中等规模 结构体封装 封装·可扩展·复杂系统 状态数 < 10 状态数 10~30 状态数 > 30 选择建议: 简单控制 → switch-case | 性能敏感 → 函数指针表 | 复杂系统 → 结构体封装

3.6 我的实战建议

嗯,最后说点实在的。这三种方式我都用过,踩过坑也填过坑。给你几个建议:

  • 别过度设计——如果状态不超过5个,switch-case就是最好的选择。我见过有人为了3个状态搞了个函数指针表,结果团队里其他人看不懂。
  • 函数指针表一定要加边界检查——我曾经在生产环境上因为数组越界导致系统崩溃,排查了两天才找到原因。血的教训。
  • 结构体封装时考虑多实例——如果你的系统有多个相同类型的状态机(比如多个串口通道),结构体封装是唯一正确的选择。
  • 调试时打印状态变化——不管用哪种方式,我都会在状态切换时加一条调试日志。这招帮我省了无数调试时间。

总结一下:switch-case是入门,函数指针表是进阶,结构体封装是工程化。从今天开始,你可以根据项目复杂度灵活选择。记住,没有最好的方式,只有最合适的方式。

公众号:蓝海资料掘金营,微信deep3321