目录
[(1) 状态定义](#(1) 状态定义)
[(2) 状态转换规则](#(2) 状态转换规则)
[(3) 输入处理](#(3) 输入处理)
[(4) 状态机的封装](#(4) 状态机的封装)
[(5) 状态机的可扩展性和维护性](#(5) 状态机的可扩展性和维护性)
[(6) 避免状态爆炸](#(6) 避免状态爆炸)
[(7) 并发和同步](#(7) 并发和同步)
[(8) 资源管理](#(8) 资源管理)
[(9) 异常处理](#(9) 异常处理)
[(10) 清晰注释和文档](#(10) 清晰注释和文档)
前阵子在改的程序已经暂时告一段落了,现在要和其他同学所做的项目联系起来,其中有一部分涉及到状态机的设计,所以简单写个笔记吧。
参考资料:
一、基础知识
1、状态机
状态机是**有限状态自动机(Finite State Machine,FSM)**的简称,通过状态图可以清晰表达整个状态的流转。
++其中涉及到四个概念:++
- 状态(state):指事物的不同状态,一个状态机至少有两个状态。例如一个灯泡,有"亮"和"灭"两种。
- 事件(event):执行某个操作的触发条件或者口令。例如对于事物"灯泡"来说,有"打开开关"和"关闭开关"两个事件。
- 动作(action):事件发生以后要执行动作。例如对于事件"打开开关",动作就是"开灯"。一般情况下,一个action一般就对应一个函数。
- 转换(transition):从一个状态转变成另一个状态。例如,"开灯过程"就是一个变换。
2、四大要素
- 现态:当前所处状态
- 次态:当条件满足后,即将转移的下一个状态
- 动作:当满足某个事件时执行的动作;动作执行完毕后可以转移到另一个状态或保持原有状态
- 条件:转移状态所需的条件,当满足条件时,会触发一个动作或进行状态转移
3、描述方式
- 状态转移图
- 状态转移表
- HDL描述
4、设计步骤
- 逻辑抽象,得到状态转移图:确定输入、输出、状态变量、画状态转移图;
- 状态简化,得到最简的状态转移图:合并等价状态;
- 状态编码;
- 用C++描述;
5、实现过程中需注意
(1) 状态定义
- 明确定义状态集合,并确保状态之间转换的完备性和一致性。
- 可以使用枚举类型来定义状态,便于理解和管理。
(2) 状态转换规则
- 清晰地定义每个状态下接收哪些输入信号以及接收到这些信号时如何转换到新的状态。
- 使用某种机制(如switch-case、查找表、函数指针等)来实现状态间的转换。
(3) 输入处理
- 确保状态机能正确响应所有有效的输入信号,并且对于无效输入有合适的默认处理策略。
- 如果存在多种输入同时有效的情况,要考虑如何优先级排序或冲突处理。
(4) 状态机的封装
- 将状态机实现封装在一个模块中,隐藏内部状态变化细节,对外暴露接口供其他部分调用。
- 使用私有变量保存当前状态,并通过公共函数改变状态。
(5) 状态机的可扩展性和维护性
- 设计时尽量使状态机易于添加新状态或修改现有状态转换逻辑。
- 使用表驱动(table-driven)方法可以提高代码的可读性和可维护性,尤其是当状态数量较多时。
(6) 避免状态爆炸
- 当状态过多时,要考虑是否存在冗余状态,尝试优化和归并相似状态,以减少状态总数。
(7) 并发和同步
- 若状态机涉及多线程或中断处理,要特别注意状态访问和修改的原子性,可能需要使用互斥锁等同步机制。
(8) 资源管理
- 如果状态机操作伴随着资源(如内存、文件句柄等)的申请和释放,务必在合适的状态转换时完成相应的清理工作,避免资源泄露。
(9) 异常处理
- 确保状态机在发生错误或异常情况下能够恢复到安全状态或报告错误。
(10) 清晰注释和文档
- 详细记录状态机的工作流程、状态转换图以及关键状态和转换的解释,有助于后期维护和他人理解代码。
二、状态机实现
在这里,我借鉴了脚本之家--《C++有限状态机实现详解》中的学生的日常生活示例。
- 事物:学生;
- 学生状态:起床、上学、吃午饭、写作业、睡觉;
- 状态之间需要执行相应的事件进行转移。
1、绘制状态转移图
2、创建状态转移的FSMIterm类
- 枚举所有状态State、所有事件Event;
- 成员变量:现态_curState、事件_event、次态_nextState;
- 成员函数:动作函数
cpp
//FSM状态项
class FSMIterm
{
friend class FSM;
//声明 FSM 类为 FSMIterm 类的朋友类
//这意味着 FSM 类可以访问 FSMIterm 类的所有成员,包括私有和保护成员
private:
//状态对应的动作函数
static void getup(){
cout << "student is getting up!" <<endl;
}
static void gotoschool(){
cout << "student is going to school!" <<endl;
}
static void havelunch(){
cout << "student is having lunch!" <<endl;
}
static void dohomework(){
cout << "student is doing homework!" <<endl;
}
static void sleeping(){
cout << "student is sleeping!" <<endl;
}
public:
//枚举所有可能的状态
enum State {
GETUP = 0,
GOTOSCHOOL,
HAVELUNCH,
DOHOMEWORK,
SLEEP
};
//枚举所有可能触发状态转换的事件
enum Events{
EVENT1 = 0,
EVENT2,
EVENT3
};
//初始化构造函数
//构造函数接受四个参数:现态curState、条件event、动作(一个指向函数的指针action)、次态nextState
//初始化列表分别用来初始化私有成员变量 _curState、_event、_action 和 _nextState
FSMIterm(State curState, Events event, void(*action)(), State nextState)
:_curState(curState), _event(event), _action(action), _nextState(nextState){}
private:
//前下划线表示为私有成员变量
State _curState; //现态
Events _event; //条件
void (*_action)(); //动作
//*action是一个指向无参数无返回值函数的指针,用于执行与当前状态相关联的动作
State _nextState; //次态
};
为了方便后面仿照写函数,对每行进行了注释。
其主要的思想还是上面的三个步骤。
3、创建有限状态机FSM类
- 成员变量:状态转移表vector<FSMIterm*> _fsmTable
- 成员函数:初始化状态转移表、状态转移、根据事件执行相应动作
cpp
class FSM
{
private:
//根据状态图初始化状态转移表
void initFSMTable(){ //负责根据状态转移规则初始化一个状态转移表
//每个 FSMIterm 实例都包含了状态转移的信息,如现态、触发事件、动作以及次态
_fsmTable.push_back(new FSMIterm(FSMIterm::GETUP, FSMIterm::EVENT1, &FSMIterm::getup, FSMIterm::GOTOSCHOOL));
_fsmTable.push_back(new FSMIterm(FSMIterm::GOTOSCHOOL, FSMIterm::EVENT2, &FSMIterm::gotoschool, FSMIterm::HAVELUNCH));
_fsmTable.push_back(new FSMIterm(FSMIterm::HAVELUNCH, FSMIterm::EVENT3, &FSMIterm::havelunch, FSMIterm::DOHOMEWORK));
_fsmTable.push_back(new FSMIterm(FSMIterm::DOHOMEWORK, FSMIterm::EVENT1, &FSMIterm::dohomework, FSMIterm::SLEEP));
_fsmTable.push_back(new FSMIterm(FSMIterm::SLEEP, FSMIterm::EVENT2, &FSMIterm::sleeping, FSMIterm::GETUP));
}
vector<FSMIterm*> _fsmTable; //定义私有变量_fsmTable,用来存储状态转移表
public:
//初始化当前状态(_curState)为指定状态(默认为 GETUP)
//立即调用 initFSMTable 方法初始化状态转移表
FSM(FSMIterm::State curState = FSMIterm::GETUP):_curState(curState){
initFSMTable();
}
//状态转移
void transferState(FSMIterm::State nextState){
_curState = nextState; 将传入的 nextState 赋值给_fsm 的现态 _curState
}
//当接收到一个事件(event)时,遍历状态转移表寻找匹配当前状态及事件的状态项
//若找到匹配项,则设置标志 flag 为真,记录对应的 action 函数指针和 nextState
void handleEvent(FSMIterm::Events event){
FSMIterm::State curState = _curState; //现态
void (*action)() = nullptr; //动作
FSMIterm::State nextState; //次态
bool flag = false;
for (int i = 0; i < _fsmTable.size(); i++){
if(event == _fsmTable[i]->_event && curState == _fsmTable[i]-> _curState){
flag = true;
action = _fsmTable[i]->_action;
nextState = _fsmTable[i]->_nextState;
break;
}
}
//找到对应的状态项,调用对应的动作函数action,然后调用transferState函数更新状态机到新状态
if(flag){
if(action){
action();
}
transferState(nextState);
}
}
//公共部分定义一个成员变量,表示有限状态机的当前状态,可供外部访问
public:
FSMIterm::State _curState;
};
4、测试FSM
cpp
//测试事件变换,用来改变传入事件的值,使其按照一定的顺序循环变化
void testEvent(FSMIterm::Events& event){
switch (event){
case FSMIterm::EVENT1:
event = FSMIterm::EVENT2; //如果事件为event1,则将事件更改为event2
break;
case FSMIterm::EVENT2:
event = FSMIterm::EVENT3;
break;
case FSMIterm::EVENT3:
event = FSMIterm::EVENT1;
break;
}
}
int main(){
FSM *fsm = new FSM(); //创建一个FSM类的实例,并将其指针存储在fsm中
auto event = FSMIterm::EVENT1;
int i = 0;
while(i < 12){
cout << "event " << event << " is coming......" <<endl;
fsm->handleEvent(event);
cout << "fsm current state is " << fsm->_curState << endl;
testEvent(event);
i++;
}
cout << "event: " << event <<endl;
cout << "curState: " << fsm->_curState <<endl; //打印当前状态
return 0;
}
5、完整代码
cpp
#include <iostream>
#include <vector>
using namespace std;
//FSM状态项
class FSMIterm
{
friend class FSM;
//声明 FSM 类为 FSMIterm 类的朋友类
//这意味着 FSM 类可以访问 FSMIterm 类的所有成员,包括私有和保护成员
private:
//状态对应的动作函数
static void getup(){
cout << "student is getting up!" <<endl;
}
static void gotoschool(){
cout << "student is going to school!" <<endl;
}
static void havelunch(){
cout << "student is having lunch!" <<endl;
}
static void dohomework(){
cout << "student is doing homework!" <<endl;
}
static void sleeping(){
cout << "student is sleeping!" <<endl;
}
public:
//枚举所有可能的状态
enum State {
GETUP = 0,
GOTOSCHOOL,
HAVELUNCH,
DOHOMEWORK,
SLEEP
};
//枚举所有可能触发状态转换的事件
enum Events{
EVENT1 = 0,
EVENT2,
EVENT3
};
//初始化构造函数
//构造函数接受四个参数:现态curState、条件event、动作(一个指向函数的指针action)、次态nextState
//初始化列表分别用来初始化私有成员变量 _curState、_event、_action 和 _nextState
FSMIterm(State curState, Events event, void(*action)(), State nextState)
:_curState(curState), _event(event), _action(action), _nextState(nextState){}
private:
//前下划线表示为私有成员变量
State _curState; //现态
Events _event; //条件
void (*_action)(); //动作
//*action是一个指向无参数无返回值函数的指针,用于执行与当前状态相关联的动作
State _nextState; //次态
};
class FSM
{
private:
//根据状态图初始化状态转移表
void initFSMTable(){ //负责根据状态转移规则初始化一个状态转移表
//每个 FSMIterm 实例都包含了状态转移的信息,如现态、触发事件、动作以及次态
_fsmTable.push_back(new FSMIterm(FSMIterm::GETUP, FSMIterm::EVENT1, &FSMIterm::getup, FSMIterm::GOTOSCHOOL));
_fsmTable.push_back(new FSMIterm(FSMIterm::GOTOSCHOOL, FSMIterm::EVENT2, &FSMIterm::gotoschool, FSMIterm::HAVELUNCH));
_fsmTable.push_back(new FSMIterm(FSMIterm::HAVELUNCH, FSMIterm::EVENT3, &FSMIterm::havelunch, FSMIterm::DOHOMEWORK));
_fsmTable.push_back(new FSMIterm(FSMIterm::DOHOMEWORK, FSMIterm::EVENT1, &FSMIterm::dohomework, FSMIterm::SLEEP));
_fsmTable.push_back(new FSMIterm(FSMIterm::SLEEP, FSMIterm::EVENT2, &FSMIterm::sleeping, FSMIterm::GETUP));
}
vector<FSMIterm*> _fsmTable; //定义私有变量_fsmTable,用来存储状态转移表
public:
//初始化当前状态(_curState)为指定状态(默认为 GETUP)
//立即调用 initFSMTable 方法初始化状态转移表
FSM(FSMIterm::State curState = FSMIterm::GETUP):_curState(curState){
initFSMTable();
}
//状态转移
void transferState(FSMIterm::State nextState){
_curState = nextState; 将传入的 nextState 赋值给_fsm 的现态 _curState
}
//当接收到一个事件(event)时,遍历状态转移表寻找匹配当前状态及事件的状态项
//若找到匹配项,则设置标志 flag 为真,记录对应的 action 函数指针和 nextState
void handleEvent(FSMIterm::Events event){
FSMIterm::State curState = _curState; //现态
void (*action)() = nullptr; //动作
FSMIterm::State nextState; //次态
bool flag = false;
for (int i = 0; i < _fsmTable.size(); i++){
if(event == _fsmTable[i]->_event && curState == _fsmTable[i]-> _curState){
flag = true;
action = _fsmTable[i]->_action;
nextState = _fsmTable[i]->_nextState;
break;
}
}
//找到对应的状态项,调用对应的动作函数action,然后调用transferState函数更新状态机到新状态
if(flag){
if(action){
action();
}
transferState(nextState);
}
}
//公共部分定义一个成员变量,表示有限状态机的当前状态,可供外部访问
public:
FSMIterm::State _curState;
};
//测试事件变换,用来改变传入事件的值,使其按照一定的顺序循环变化
void testEvent(FSMIterm::Events& event){
switch (event){
case FSMIterm::EVENT1:
event = FSMIterm::EVENT2; //如果事件为event1,则将事件更改为event2
break;
case FSMIterm::EVENT2:
event = FSMIterm::EVENT3;
break;
case FSMIterm::EVENT3:
event = FSMIterm::EVENT1;
break;
}
}
int main(){
FSM *fsm = new FSM(); //创建一个FSM类的实例,并将其指针存储在fsm中
auto event = FSMIterm::EVENT1;
int i = 0;
while(i < 12){
cout << "event " << event << " is coming......" <<endl;
fsm->handleEvent(event);
cout << "fsm current state is " << fsm->_curState << endl;
testEvent(event);
i++;
}
cout << "event: " << event <<endl;
cout << "curState: " << fsm->_curState <<endl; //打印当前状态
return 0;
}
6、测试结果
原版中的状态机是个无限循环状态,在这里我是定义了一个int型整数,只执行12次(至于为什么是12次,刚开始以为是4种状态,可以每个状态执行3遍,运行后发现是5个状态/扶额苦笑)
三、多分支状态机实现
前面的状态机实现是单个分支的,为了方便后续复杂状态机的开发,所以我新增了一个分支路,即:状态GOTOSCHOOL--->(EVENT4驱动)--->状态PLAYGAME--->(EVENT5驱动)--->状态DOHOMEWORK。同时为状态PLAYGAME添加一个一个静态函数playinggame()。
需要修改的部分
- 在
FSMIterm
类中添加新的静态动作函数playinggame
。 - 在
FSMIterm
的枚举中添加EVENT4
和EVENT5
。 - 在
FSMIterm
的枚举中添加PLAYGAME
状态。 - 在
FSM
类的initFSMTable
函数中添加新的状态转移项。 - 更新
testEvent
函数以处理新的事件。