C 语言状态机实践
更新时间:2026-08-26。本文是
languages/c/主题高手层文档。
本文要回答的问题
- 状态机解决什么问题?什么时候该用?
- 用
switch写状态机为什么容易写成长毛怪? - 状态转移表是什么?它凭什么更优雅?
一、状态机模型
状态机 = 状态 × 事件 → 新状态(+动作)。核心是把"一堆 if/else 纠缠的逻辑"显式拆成状态和转移:
c
typedef enum { S_IDLE, S_RUN, S_ERR } State;| 应用 | 状态 | 事件 |
|---|---|---|
| 协议解析 | 等待头/读长度/读载荷 | 字节到达 |
| 输入校验 | 起始/数字/小数/结束 | 字符类型 |
| 网络连接 | 监听/握手/就绪/关闭 | 报文 |
以"协议解析"为例,把状态机画成图就一目了然:S_IDLE 收到头字节进入 S_RUN,遇到非法输入掉进 S_ERR,正常收完回到空闲——事件就是箭头上的标注:

图里每个箭头都标了"什么事件触发转移",这正是状态机的核心——把"逐字节条件判断"显式成"状态×事件→新状态",比一堆 if/else 纠缠清楚得多。
二、两种实现
| 方式 | 结构 | 优点 | 缺点 |
|---|---|---|---|
| switch 嵌套 | switch(state){ case ...: switch(event) | 直观、好读 | 状态×事件多了像毛线球 |
| 转移表 | 二维表:state × event → (next_state, handler) | 数据驱动、易扩展 | 要先设计表 |
转移表本质是 函数指针数组:
c
typedef struct { State next; void (*action)(void *ctx); } Trans;
static Trans table[ST_MAX][EV_MAX];主循环变成查表调用,新增状态/事件只需改表,不用动逻辑。
三、使用要点
- 状态要穷举、事件要穷举:查表得先确定越界处理。
- 动作(副作用)与状态跳转分离:先执行 action 再转移。
- 用 enum 代替裸整数,可读性大增,见 枚举。
实测:switch 状态机解析坐标
用状态机解析 "数字,数字" 格式的坐标:
c
typedef enum { S_INT1, S_COMMA, S_INT2, S_DONE, S_ERR } State;
// 状态机:S_INT1 读第一个数 → S_COMMA 遇逗号 → S_INT2 读第二个数
State parse_coord(const char *s, int *x, int *y) {
State st = S_INT1;
int v1 = 0, v2 = 0;
for (const char *p = s; *p; p++) {
switch (st) {
case S_INT1:
if (c >= '0' && c <= '9') v1 = v1 * 10 + (c - '0');
else if (c == ',') st = S_COMMA;
else st = S_ERR;
break;
/* ... 其余状态 ... */
}
}
return st;
}实测:
text
解析 "123,456": 成功, x=123 y=456 ← 正常解析
解析 "12,": 失败 ← 缺第二个数,正确拒绝
解析 "abc,1": 失败 ← 非数字,正确拒绝状态机把"逐字符解析"的复杂条件,显式拆成"当前状态 + 输入 → 下一状态",每个 case 只处理一种状态下的输入,逻辑清晰、边界明确。
四、与入门层的衔接
- 枚举与状态常量,见 枚举类型。
- switch 语法与陷阱,见 switch-case。
- 转移表与函数指针,见 函数指针进阶。
一句话总结
状态机把纠缠的条件逻辑变成"状态×事件"的显式表格:小状态机用 switch 够用,状态一多就上转移表,让新增分支变成改表而不是改逻辑。
本文已完成填充:示例代码与实测数据已补齐。