C++状态机实现全解析:从switch到层次状态机的工程实践
做C时间长了你会发现很多项目代码变乱是从一个状态一个flag开始的。flag一多行为就互相打架你改了A状态的退出逻辑B状态莫名其妙开始抽风。状态机FSM有限状态机是解决这类问题的成熟工具但它在C里具体怎么落地不是一句话能说清的——有简单的switch-case有函数指针表有面向对象的状态模式还有Boost.Statechart和SML这类专门的状态机库。这篇文章我打算把这几种方案都过一遍从最基础的写法讲到工程化的层次状态机讲清楚各自的核心原理、代码长什么样、适用边界以及我实际用下来踩过的坑。适合正在写业务逻辑、搞嵌入式、或者维护老C代码的工程师参考选型前拿来做个对照。1. 状态机的本质和C里的落地思路1.1 状态机到底解决什么问题先聊一个最朴素的问题为什么非要引入状态机这个概念直接写逻辑不行吗我在以前一个项目里维护过一套串口设备管理模块。设备有三个阶段空闲、打开、传输中。三个bool变量记录这些阶段再加一个错误标志一个忙碌标志。刚开始写起来特别顺手加几个if判断就好。但后来需求一变加了打开超时重试和传输中断恢复两个功能原来的if-else立刻崩了——一个标志位满足不了多个状态组合经常出现看起来既在打开又在传输中的诡异局面。状态机的价值就在这里。它把对象的行为空间划分成有限个互斥的状态一次只处于一个状态。事件进来时以当前状态事件为索引决定做什么动作、转移到哪个状态。这本质上是一个二维表穷举了所有合法组合那些不可能发生的组合直接变成非法事件可以被捕获、记录、或者忽略。行为可预期、可回溯、可测试这就是它比散装flag强一大截的原因。1.2 C实现状态机的四条技术路线C里没有标准库的状态机组件所以社区里沉淀出了几种主流写法。我按工程成熟度和实现复杂度排一下方案实现方式适宜规模主要缺点switch-case枚举状态 大switch分发10个状态以内状态多了极难维护函数指针/表驱动状态映射到处理函数中型状态机类型安全性一般状态模式每个状态一个类虚函数分发中型偏大类爆炸代码啰嗦第三方库Boost.Statechart / SML复杂层次状态机学习成本高编译慢这4条路线并不互斥实际生产里经常混用。比如嵌入式场景用表驱动上层逻辑需要复杂迁移时用SML。我的建议是技术选型不要追新先看状态数量、迁移复杂度、团队对代码的熟悉程度。下面我会逐一拆解每种写法配上可运行的例子你就知道该怎么选了。2. switch-case写状态机小规模场景的务实选择2.1 电梯门控制状态机的完整示例switch-case是最直接、最不需要额外框架的状态机写法。核心是用一个枚举变量表示当前状态事件来了就进入switch在对应case里判断事件、执行动作、返回新状态。以电梯门控制为例。门有4个状态关门中、关门到位Closed、开门中、开门到位Open。事件有开门按钮、关门按钮、门到位信号、超时信号。用C写出来大致是enum class DoorState { Closing, // 关门中电机正转 Closed, // 完全关闭 Opening, // 开门中电机反转 Open // 完全打开 }; enum class DoorEvent { ButtonOpen, // 乘客按了开门键 ButtonClose, // 乘客按了关门键 LimitReached, // 门到达极限位置 OpenTimeout // 开门停留超时 }; struct DoorContext { void startMotor(bool closing) { /* 控制电机true为关门方向 */ } void stopMotor() { /* 停电机 */ } void startTimer(uint32_t ms) { /* 启动超时定时器 */ } }; DoorState handleDoorEvent(DoorState state, DoorEvent ev, DoorContext ctx) { switch (state) { case DoorState::Closed: if (ev DoorEvent::ButtonOpen) { ctx.startMotor(false); // 向开门方向转 return DoorState::Opening; } break; case DoorState::Opening: if (ev DoorEvent::LimitReached) { ctx.stopMotor(); ctx.startTimer(5000); // 开门保持5秒 return DoorState::Open; } break; case DoorState::Open: if (ev DoorEvent::ButtonClose) { ctx.startMotor(true); return DoorState::Closing; } if (ev DoorEvent::OpenTimeout) { ctx.startMotor(true); return DoorState::Closing; } break; case DoorState::Closing: if (ev DoorEvent::LimitReached) { ctx.stopMotor(); return DoorState::Closed; } break; } return state; // 未处理的事件不改变状态 }这段代码有几个值得注意的点。一是状态迁移在一个函数里集中管理你一眼能看出关门中收到限位信号就变关门到位这个规则。二是动作电机、定时器被放到Context结构里状态机函数本身不直接操作硬件后续做单元测试时可以用Mock替换Context。三是switch的每个分支都写全不遗漏事件这样编译器无法帮忙检查漏状态但代码review的时候可以。2.2 switch-case方案翻车的三个典型场景switch-case方案看着简单实际项目里用着用着就会遇到几个坎。第一是状态数量上来之后的可维护性问题。我见过一个设备控制程序状态多达20个事件15个handleDoorEvent这类函数膨胀到800多行。每次想查某个状态下某个事件会发生什么得在case里翻半天。更难受的是有时候两个case分支里有重复逻辑比如多个状态都要响应用户取消事件你只能复制粘贴粘贴一多改一处漏一处。第二是转移逻辑和业务副作用混在一起。上面的电梯门例子还算克制实际代码里大家喜欢在状态机函数里直接写日志、发消息、置寄存器。表面上方便实际你没法单独测试一次迁移——它把所有事情都做完了。后面讲到表驱动和状态模式时你会发现解耦的意义就在这里。第三是非法事件的处理没有统一策略。switch写多了没匹配到的分支通常就是break返回原状态但到底是忽略还是报错代码里看不出意图。如果某些非法事件其实意味着程序逻辑出错了你最好在default分支里加日志。我后来习惯在switch.default里统一打warning线上出了诡异问题日志一翻就能发现是哪个状态收到了不期望的事件。所以我的结论10个状态以内、事件类型简单、没有复杂嵌套迁移的场景switch-case完全够用不用上框架。超过这个规模请继续往下看。3. 函数指针表驱动状态机的工程化第一步3.1 核心思路把switch变成状态函数集合函数指针表驱动也叫表驱动状态机是嵌入式C/C里非常常见的一种方案。它的核心想法是每个状态对应一个处理函数函数内部处理本状态下所有可能要处理的事件并返回下一个状态的处理函数指针。这有什么好处首先原来那个巨大的switch被拆成了若干个函数每个函数只关心一个状态局部性变好。其次状态迁移不再写在一个集中式的大switch里而是分布在每个状态函数内部改一个状态的行为只动一个函数不会牵扯到其他状态。最后状态机本身变成一个当前处理函数指针在事件循环里统一调用代码结构天然统一。用伪码表达一下struct Fsm; using StateHandler void (*)(Fsm fsm, const Event ev); struct Fsm { StateHandler current; // 当前状态对应的处理函数 }; void runEvent(Fsm fsm, const Event ev) { fsm.current(fsm, ev); }状态迁移就是给current赋一个新的函数指针下一个事件进来时就会进入新的处理函数。可以理解为状态机自己不再关心全局迁移表每个状态只负责自己的这一小段逻辑。3.2 一个按键状态机单击、双击、长按的完整实现按键处理是状态机最经典的嵌入式场景。一颗按键要区分单击、双击、长按、长按连发用if-else写会非常痛苦。我用表驱动写一个简化版本你可以感受一下这个模式。enum class KeyEventType { Press, Release, TimerTick }; struct KeyEvent { KeyEventType type; uint32_t timeMs; // 事件发生时的系统时间 }; // 前置声明因为状态函数之间要互相跳转 void keyIdle(KeySM sm, const KeyEvent ev); void keyPressedDebounce(KeySM sm, const KeyEvent ev); void keyLongPress(KeySM sm, const KeyEvent ev); void keyWaitSecondPress(KeySM sm, const KeyEvent ev); struct KeySM { void (*state)(KeySM, const KeyEvent); // 当前状态入口 uint32_t pressTime; // 按下时间 uint32_t releaseTime; // 松开时间 bool longPressHandled; }; void keyIdle(KeySM sm, const KeyEvent ev) { if (ev.type KeyEventType::Press) { sm.pressTime ev.timeMs; sm.longPressHandled false; sm.state keyPressedDebounce; // 进入按下消抖状态 } } void keyPressedDebounce(KeySM sm, const KeyEvent ev) { if (ev.type KeyEventType::Release) { // 短时间内松手认为是单击等双击判定 sm.releaseTime ev.timeMs; sm.state keyWaitSecondPress; } else if (ev.type KeyEventType::TimerTick ev.timeMs - sm.pressTime 800) { // 按住超过800ms判定为长按 sm.state keyLongPress; } } void keyLongPress(KeySM sm, const KeyEvent ev) { if (ev.type KeyEventType::TimerTick !sm.longPressHandled) { onLongPressTriggered(ev.timeMs); // 触发一次长按 sm.longPressHandled true; } else if (ev.type KeyEventType::Release) { sm.state keyIdle; // 松手回到空闲 } } void keyWaitSecondPress(KeySM sm, const KeyEvent ev) { if (ev.type KeyEventType::Press) { // 双击成功 onDoubleClick(ev.timeMs); sm.state keyIdle; } else if (ev.type KeyEventType::TimerTick ev.timeMs - sm.releaseTime 300) { // 超过300ms没有再按下判定为单击 onSingleClick(releaseTime); sm.state keyIdle; } }这个示例把每种事件在每个状态下该干什么拆散到各个函数中每个函数只处理一个状态。要加一个新状态直接写一个新函数替换指针跳转就行不完全动其他函数。这种结构的最大价值是状态函数之间边界清晰——你不太容易在改长按逻辑时误伤双击逻辑。3.3 表驱动方案的两个关键陷阱表驱动虽然好用但有几个地方特别容易踩坑。头一个是函数指针类型。实际工程里不同状态可能需要访问不同的上下文变量函数签名弄成void(*)()很简单但类型安全就没了。我建议把签名固定成void(*)(FsmImpl, const Event)所有状态数据塞进FsmImpl结构体这样函数指针类型统一事件也能携带参数后续加字段只需要改结构体不用动所有函数。第二个陷阱是初始状态。很多人的表驱动状态机没有显式初始化current指针结果第一个事件进来调用了一个空指针程序直接崩。排查半天才发现是创建状态机时忘了赋初始状态。我的习惯是在状态机的构造函数里强制设置初始状态比如fsm.state keyIdle;并且在runEvent入口处检查一次current非空打日志告警。这种防御性写法在长生命周期的嵌入式系统里非常值钱。还有一个小细节状态函数里可能要做耗时操作但事件驱动的调用都是同步的如果某个状态函数执行太久下一个事件就会被阻塞。按键消抖这种短逻辑没问题如果是网络协议栈的状态机就要考虑把耗时操作放到任务队列里不要堵在状态函数里。4. 面向对象的状态模式正统但别滥用4.1 一个完整的C状态模式示例状态模式是GoF设计模式里的经典它把每个状态封装成一个类状态机的上下文类持有当前状态对象的引用事件到来时直接调用当前状态的虚函数。这样一来每个状态的逻辑完全独立增加新状态只需要新增一个类不用修改现有代码。我用一个电梯门重新写一遍状态模式方便和上面的switch版本对比。为了简化只保留核心框架。class ElevatorContext; class DoorState { public: virtual ~DoorState() default; virtual std::unique_ptrDoorState handleOpenButton(ElevatorContext) { return nullptr; } virtual std::unique_ptrDoorState handleCloseButton(ElevatorContext) { return nullptr; } virtual std::unique_ptrDoorState handleLimitReached(ElevatorContext) { return nullptr; } virtual std::unique_ptrDoorState handleTimeout(ElevatorContext) { return nullptr; } }; class ClosedState : public DoorState { public: std::unique_ptrDoorState handleOpenButton(ElevatorContext ctx) override; // close按钮在关闭状态下无效 }; std::unique_ptrDoorState ClosedState::handleOpenButton(ElevatorContext ctx) { ctx.startMotor(false); return std::make_uniqueOpeningState(); } class OpeningState : public DoorState { public: std::unique_ptrDoorState handleLimitReached(ElevatorContext ctx) override { ctx.stopMotor(); ctx.startTimer(5000); return std::make_uniqueOpenState(); } }; class OpenState : public DoorState { public: std::unique_ptrDoorState handleCloseButton(ElevatorContext ctx) override { ctx.startMotor(true); return std::make_uniqueClosingState(); } std::unique_ptrDoorState handleTimeout(ElevatorContext ctx) override { ctx.startMotor(true); return std::make_uniqueClosingState(); } }; class ElevatorContext { public: void startMotor(bool closing) { /* ... */ } void stopMotor() { /* ... */ } void startTimer(uint32_t ms) { /* ... */ } void onOpenButton() { if (auto next state_-handleOpenButton(*this)) { state_ std::move(next); } } void onCloseButton() { if (auto next state_-handleCloseButton(*this)) { state_ std::move(next); } } // ... 其他事件入口 private: std::unique_ptrDoorState state_ std::make_uniqueClosedState(); };状态模式的优点很明显开闭原则贯彻得彻底新增状态不用动已有状态类每个状态类的内部变量独立不像switch版本那样所有状态共享一个上下文结构体避免变量污染。4.2 状态模式在真实项目里的两个硬伤但状态模式不是银弹我在项目里用了几次之后逐渐发现它有几个绕不开的问题。第一个是类数量爆炸。一个10状态、20事件的状态机你要写至少10个类每个类还要实现所有事件的接口。哪怕某个状态下某个事件完全非法你也要写一个返回nullptr的override。代码量比switch版本大了好几倍团队新人上手想搞清楚整个状态机全貌得打开十几个头文件来回跳。第二个是性能开销。每个事件处理都是一个虚函数调用状态迁移要new一个子类对象老对象要被delete。如果状态迁移频繁比如网络收发帧时状态不停地切堆分配和释放会带来不必要的延迟和内存碎片。后来我为了规避这个问题改用对象池或者直接把状态对象作为Context的成员变量而不是指针动态分配才把性能损耗压下来。所以我现在的建议是状态模式适合那种状态类本身需要持有大量独立业务字段、且状态迁移不频繁的模块。比如订单状态、审批流这类对象生命周期长、迁移次数少的场景很合适。如果状态迁移每秒发生几千次还是老老实实用表驱动或者第三方库吧。5. 层次状态机复杂业务下最实用的进阶套路5.1 什么是层次为什么需要层次先举一个我实际做过的例子。一个网络通信模块有已连接这个大状态里面又细分了正在认证传输数据中重连等待中三个子状态。普通FSM会把这4个都列成平级状态于是你发现一个问题不管在哪个子状态只要收到对端关闭连接事件都要统一做释放资源、断开链路、进入空闲这三件事。如果平级写你需要在3个状态的处理逻辑里各写一遍释放资源断开链路重复三次。如果以后改成对端关闭前要发一条通知就得改3个地方漏改一个就是线上事故。层次状态机HSMHierarchical State Machine就是为了解决这种重复。它允许状态嵌套父状态表示一个大的业务阶段子状态表示该阶段内的细节。当一个事件在当前子状态中没有被处理时它会向父状态传播由父状态处理。这样关闭连接这个公共行为只需要写在父状态的处理逻辑里一次所有子状态自动继承。5.2 理解HSM的事件传播机制HSM的核心规则其实就一条事件冒泡。子状态优先处理子状态不处理交给父状态父状态也不处理继续上抛直到根状态。你把这个规则记住HSM就能看懂了。写出一个伪码实现struct HsmState { HsmState* parent; void (*handler)(HsmState*, const Event); }; void dispatchEvent(HsmState* current, const Event ev) { HsmState* cursor current; while (cursor) { cursor-handler(cursor, ev); // 如果handler返回true表示事件已处理终止冒泡 if (ev.handled) return; cursor cursor-parent; // 否则上抛父状态 } }比如网络连接模块已连接是父状态其中传输数据中是子状态。收到数据帧事件时子状态的handler处理掉收到对端关闭事件时子状态的handler不做处理事件冒泡到父状态父状态统一做资源回收和状态切换。这样一来子状态只需要关心自己的细节事件公共事件统统往上收。5.3 手写一个极简HSM的工程示范上一节的伪码能说明原理但真要在工程里用你需要处理进入状态时的动作和退出状态时的动作。我提供一个我经常用的简化模型每个状态节点包含enter、exit、handle三个函数指针状态迁移通过宏或者辅助函数完成。struct HsmNode { const char* name; HsmNode* parent; void (*enter)(void* ctx); void (*exit)(void* ctx); bool (*handle)(void* ctx, const Event ev); // 返回true表示已消费事件 }; struct Hsm { HsmNode* current; }; void hsm_transition(Hsm* hsm, HsmNode* next, void* ctx) { if (hsm-current hsm-current-exit) { hsm-current-exit(ctx); // 退出旧状态 } hsm-current next; if (next next-enter) { next-enter(ctx); // 进入新状态 } }使用这个模型时你需要为每个状态定义HsmNode并在handle函数里实现状态内的转移。它的好处是每个状态只关心自己的一亩三分地公共逻辑交给父节点代码的复用率一下就上去了。但手写HSM还是有一些繁琐之处尤其是enter/exit动作在父子状态间如何协调进入父状态时要不要执行父状态的enter退出时先后顺序怎么定这些都容易写错。如果你的项目里HSM的层次超过三层我建议直接用成熟的库别手造轮子。6. 第三方库选型Boost.Statechart 与 SML 实战对比6.1 Boost.Statechart功能全但笨重Boost.Statechart是Boost库里的官方状态机组件最大的特点是完整的HSM支持状态可嵌套、可正交分解enter/exit动作框架自动管理事件转发。它内部大量使用模板元编程所以功能非常强大但付出的代价也很明显——编译时间变长、宏和模板错误信息非常难调、二进制体积也偏大。一个典型的最小Boost.Statechart状态机长这样#include boost/statechart/state_machine.hpp #include boost/statechart/state.hpp #include boost/statechart/transition.hpp namespace sc boost::statechart; struct Idle; struct Running; struct HelloMachine : sc::state_machineHelloMachine, Idle {}; struct Idle : sc::stateIdle, HelloMachine { using reactions sc::transitionEvStart, Running; Idle(my_context ctx) : my_base(ctx) {} }; struct Running : sc::stateRunning, HelloMachine { using reactions sc::transitionEvStop, Idle; Running(my_context ctx) : my_base(ctx) {} };看着简洁但你一旦要写复杂的enter/exit、post-event、guard条件模板的嵌套深度立刻翻倍。我个人的经验是如果你的团队里没人熟读Boost.Statechart的文档别轻易上出了问题排查成本很高。6.2 SML编译期元编程一个新的选项SMLBoost.SML是另一个流行的状态机库它和Boost.Statechart走了完全不同的路线。SML利用模板元编程在编译期生成状态机表C14以上即可使用语法更贴近DSL代码量也少。下面这个例子展示它定义状态机的方式#include boost/sml.hpp namespace sml boost::sml; struct idle; struct running; struct Start {}; struct Stop {}; struct Machine { auto operator()() const { using namespace sml; return make_transition_table( *stateidle eventStart / [] { startWork(); } staterunning, staterunning eventStop / [] { stopWork(); } stateidle ); } }; sml::smMachine machine; machine.process_event(Start{}); machine.process_event(Stop{});SML的写法非常直观状态、事件、guard、动作全都在一张转换表里。它适合状态迁移规则多、逻辑复杂的模块开发效率很高。缺点是编译错误信息依旧长模板元编程的通病以及库版本更新速度一般有些高级特性需要翻源码确认。6.3 选型建议与实践心得我的选型判断如下状态少于10个迁移规则一目了然直接用switch-case或者表驱动不需要引第三方库。状态多且有层次团队熟悉BoostBoost.Statechart能扛住但一定要有人懂它的机制否则维护成本太高。要快速实现复杂迁移、模板元编程对团队不是黑盒SML是值得投入的方向。嵌入式资源受限、没有RTTI/异常环境不推荐上这两个库用表驱动手写更稳妥。还有一点不管用哪个库状态机的顶层设计一定要先画出来。我见过项目用了SML但状态划分混乱得像蜘蛛网最后还不如switch清晰。工具只解决怎么写的问题解决不了设计成什么样的问题。7. 状态机工程落地的三个关键基建7.1 状态命名与日志让程序自己告诉你它在哪里状态机调试最大的痛点是状态和事件都是枚举或函数指针出问题了你看到的是一条日志[FSM] event3, state5你根本不知道3和5分别代表什么。如果程序跑在客户现场靠这个日志排查问题能让工程师当场崩溃。我自己的做法是给每个状态和事件维护一个name字符串表const char* stateName(State s) { switch (s) { case State::Idle: return Idle; case State::Running: return Running; // ... } } const char* eventName(Event e) { switch (e) { case Event::Start: return Start; case Event::Stop: return Stop; // ... } }每次状态迁移时打印一条格式化日志[FSM] Idle --Start-- Running。这样线上日志拉起整个状态机行为一目了然。别嫌这个工具有点多它一次能帮你省下几个小时的排查时间。7.2 状态机的测试策略覆盖迁移矩阵状态机相比普通逻辑有一个明显优势它天然适合穷举测试。你可以把状态和事件的组合整理成一张矩阵然后逐项验证。我常用的测试方式是构造一个迁移表测试用表格驱动的方式跑所有合法迁移TEST_Fsm, TransitionMatrix) { struct Case { State from; Event ev; State expectTo; }; std::vectorCase cases { {State::Idle, Event::Start, State::Running}, {State::Running, Event::Stop, State::Idle}, {State::Idle, Event::Stop, State::Idle}, // 非法事件保持原位 }; for (const auto c : cases) { auto fsm createFsmWithState(c.from); fsm.handle(c.ev); EXPECT_EQ(fsm.currentState(), c.expectTo); } }关键点非法事件的测试要单独写。很多bug不是合法迁移没做对而是非法事件导致状态机崩溃或者状态错乱。测试时故意发一堆不该发生的事件验证状态机不崩、不改变当前状态这个保护比验证合法迁移更重要。7.3 状态机里尽量不要写耗时操作最后一条经验也是我早期吃过亏的地方状态机的处理函数应该是短小快速的。它负责业务决策和状态变更不要把文件写入、网络发送、复杂计算直接塞进去。原因很简单状态机常常是事件循环的中心一个状态函数耗时太久其他事件全被阻塞。哪怕你没有多线程消息延迟也会让人感觉系统卡顿。我现在的习惯是状态函数里只做判定投递两件事把真正耗时的动作派发给工作线程或者异步任务。状态机只管业务状态处理细节交给其他模块。这样状态机保持轻快也更容易测试和回放。8. 常见问题与排查技巧实录8.1 状态反复横跳典型现象是状态机在A和B之间来回切换稳定不下来。我在锁文件同步模块里遇到过——状态每触发一次就迁移回去形成了死循环。排查方法看日志里的状态迁移序列。如果日志显示Idle - Running - Idle - Running就看触发Running的事件从哪来。往往是某个事件在状态入口被重复派发或者迁移后的新状态立即触发了旧事件的回执。解决办法是给事件加唯一ID避免同一个事件被处理两次或者在迁移后清除待处理事件队列。8.2 状态丢失现象是状态机莫名其妙回到初始状态可能发生在对象生命周期管理不当的地方。最常见的原因是Context被局部变量持有析构后状态机被销毁但外面还有一份悬空指针在调用。排查办法开AddressSanitizerASan跑一轮测试基本能立刻定位到悬空指针。根治做法是让状态机拥有明确的所有权关系——通常用unique_ptr持有由管理它的模块统一创建和销毁不要随手在函数里new一个局部状态机往外传。8.3 多线程并发事件导致状态错乱多线程环境下同一时刻有两个线程向状态机分发事件本来互斥的状态就错乱了。很多人会用全局锁包住整个状态机分发函数但这样等于把所有事件串行化性能损失大。如果转移逻辑本身不复杂我会用无锁队列把事件排成单流由一个专门线程处理状态机事件。如果状态机本来就要在多线程里响应那就得在状态机内部按状态划分锁的粒度或者干脆重新设计架构——说到底状态机模型天然是单线程的硬在多线程里用问题会非常多。9. 从项目实战里沉淀的一点经验我把这些年写状态机的经历整理一下最值钱的几条建议放在最后方便你直接抄作业状态机的状态划分不要过度细化。状态应该是可观测的业务阶段不是某个动作的中间步骤。我见过有人把一个简单的请求流程拆成15个状态结果每步的迁移逻辑只有一行徒增维护成本。先画状态迁移图再写代码。哪怕你只是用纸笔画个草图也比直接写代码强十倍。无状态机编程时flag多了就重构。不要等到flag之间互相打架怀疑人生才动手。用C写状态机时要选择符合团队当前维护水平的方案。我见过几人团队为了炫技上SML结果三个月后没人敢碰那套模板代码。C里状态机的实现路子很多从最基础的switch到Boost.Statechart这种工业级框架没有绝对的对错只有合不合适。希望这篇文章能帮你少踩几个坑。如果你正在做一个状态机不妨先画出状态迁移图再套用这里面的某一种写法你的代码会清爽很多。