4、C语言实现基础:switch-case实现状态机、函数指针表实现状态机、两种方式的对比
好,咱们进入实战环节了。前面几章讲了状态机的理论,这一章咱们就用C语言把它真正跑起来。我个人习惯把状态机的实现分成两派:switch-case派和函数指针表派。两种我都用过,各有各的脾气。
4.1 switch-case实现状态机
这是最直观的方式。说白了,就是用switch语句把每个状态和对应的行为写在一起。我刚开始做嵌入式开发时,第一个状态机就是用switch-case写的。
来看一个简单的例子:一个LED灯的状态机,有关闭、亮起、闪烁三个状态。
// 定义状态枚举
typedef enum {
STATE_LED_OFF,
STATE_LED_ON,
STATE_LED_BLINK
} led_state_t;
// 定义事件枚举
typedef enum {
EVENT_BUTTON_PRESS,
EVENT_TIMEOUT,
EVENT_NONE
} led_event_t;
// 状态机主函数
led_state_t led_state_machine(led_state_t current_state, led_event_t event) {
led_state_t next_state = current_state;
switch (current_state) {
case STATE_LED_OFF:
if (event == EVENT_BUTTON_PRESS) {
next_state = STATE_LED_ON;
// 执行进入亮起状态的动作
led_turn_on();
}
break;
case STATE_LED_ON:
if (event == EVENT_BUTTON_PRESS) {
next_state = STATE_LED_BLINK;
// 执行进入闪烁状态的动作
led_start_blink();
} else if (event == EVENT_TIMEOUT) {
next_state = STATE_LED_OFF;
led_turn_off();
}
break;
case STATE_LED_BLINK:
if (event == EVENT_BUTTON_PRESS) {
next_state = STATE_LED_OFF;
led_stop_blink();
led_turn_off();
}
break;
default:
// 异常处理
next_state = STATE_LED_OFF;
led_turn_off();
break;
}
return next_state;
}
核心要点:switch-case状态机的本质就是状态+事件→动作+下一状态的映射表。每个case分支处理一个状态下的所有事件。
我在项目中遇到过一个问题:当状态数量超过10个时,这个函数会变得非常长。你想想看,每个状态下面可能有好几个事件,代码一多就容易漏掉某个事件的处理。
我的小技巧:如果状态少于8个,switch-case是最清晰的选择。代码可读性高,调试也方便。我一般会在每个case后面加注释,标明当前状态和预期行为。
4.2 函数指针表实现状态机
这种方式更高级一些。它把每个状态的处理函数单独拎出来,然后用一个函数指针数组把它们串起来。说白了,就是用查表代替了switch-case的分支跳转。
同样的LED例子,用函数指针表来实现:
// 定义状态处理函数类型
typedef led_state_t (*state_handler_t)(led_event_t);
// 前向声明
led_state_t led_off_handler(led_event_t event);
led_state_t led_on_handler(led_event_t event);
led_state_t led_blink_handler(led_event_t event);
// 状态处理函数表
state_handler_t state_table[] = {
led_off_handler, // STATE_LED_OFF
led_on_handler, // STATE_LED_ON
led_blink_handler // STATE_LED_BLINK
};
// 各个状态的处理函数
led_state_t led_off_handler(led_event_t event) {
if (event == EVENT_BUTTON_PRESS) {
led_turn_on();
return STATE_LED_ON;
}
return STATE_LED_OFF;
}
led_state_t led_on_handler(led_event_t event) {
if (event == EVENT_BUTTON_PRESS) {
led_start_blink();
return STATE_LED_BLINK;
} else if (event == EVENT_TIMEOUT) {
led_turn_off();
return STATE_LED_OFF;
}
return STATE_LED_ON;
}
led_state_t led_blink_handler(led_event_t event) {
if (event == EVENT_BUTTON_PRESS) {
led_stop_blink();
led_turn_off();
return STATE_LED_OFF;
}
return STATE_LED_BLINK;
}
// 状态机主函数
led_state_t led_state_machine(led_state_t current_state, led_event_t event) {
// 直接用状态索引调用对应的处理函数
return state_table[current_state](event);
}
核心要点:函数指针表把状态和处理逻辑解耦了。每个状态的处理函数是独立的,新增状态只需要添加一个函数和表项,不需要修改主函数。
嗯,这里要注意:函数指针表虽然灵活,但调试起来稍微麻烦一点。我曾经在一个项目中用了30多个状态的函数指针表,结果有个状态的处理函数返回了错误的状态值,查了半天才发现是表索引写错了。
避坑指南:使用函数指针表时,一定要确保枚举值和表索引一一对应。我曾经因为枚举定义顺序和表顺序不一致,导致状态跳转完全错乱。建议在枚举定义后面加注释,标明对应的表索引。
4.3 两种方式的对比
两种方式各有千秋,我根据自己的经验总结了一个对比表:
| 对比维度 | switch-case方式 | 函数指针表方式 |
|---|---|---|
| 代码可读性 | 状态少时很好,状态多时变差 | 状态多时依然清晰 |
| 执行效率 | 编译器优化后通常很快 | 直接查表,速度稳定 |
| 代码维护性 | 新增状态需要修改主函数 | 新增状态只需添加函数和表项 |
| 调试难度 | 容易单步跟踪 | 需要查看函数指针指向 |
| 代码量 | 状态少时代码量少 | 状态多时代码量更少 |
| 适用场景 | 状态数<10,逻辑简单 | 状态数≥10,逻辑复杂 |
我个人习惯这样选:
- 小项目、状态少(比如5个以内):用switch-case,简单直接
- 中等项目(6-15个状态):看心情,两种都可以
- 大项目、状态多(15个以上):强烈推荐函数指针表
我的经验之谈:如果你刚开始学状态机,先用switch-case把逻辑跑通。等你对状态机的理解深了,再尝试函数指针表。我见过太多新手一上来就用函数指针表,结果把自己绕晕了。
为什么会这样?因为switch-case是顺序执行的,你一眼就能看到整个状态机的全貌。而函数指针表把逻辑分散到各个函数里,需要跳来跳去看,对新手不太友好。
最后说一句:两种方式没有绝对的好坏。我在实际项目中,有时候会混合使用——核心状态用switch-case,扩展状态用函数指针表。灵活运用才是王道。