状态机的 C++ 代码

Posted

技术标签:

【中文标题】状态机的 C++ 代码【英文标题】:C++ code for state machine 【发布时间】:2013-01-18 13:27:04 【问题描述】:

这是一个用 C++ 编写的面试问题:

为自动售货机编写代码:从一个简单的自动售货机开始,它只售卖一种类型的商品。所以两个状态变量:金钱和库存,就可以了。

我的回答:

我会使用具有大约 3-4 个状态的状态机。使用枚举变量来指示状态并使用 switch case 语句,其中每个 case 都有对应于每个状态的操作,并停留在循环中以从一个状态移动到另一个状态。

下一个问题:

但是使用 switch case 语句并不能“很好地扩展”更多的状态被添加和修改一个状态中的现有操作。你将如何处理这个问题?

当时我无法回答这个问题。但后来想,我大概可以:

对不同的状态有不同的功能(每个功能对应一个状态) 有一个来自 (string, function) 的std::map,其中 string 指示状态以调用相应的状态函数。 主函数有一个字符串变量(从初始状态开始),并在循环中调用与该变量对应的函数。每个函数执行所需的操作并将新状态返回给主函数。

我的问题是:

switch-case 语句在大规模软件系统环境中的可伸缩性方面存在什么问题? 如果是这样,我的解决方案(目前我觉得它比长线性代码更模块化)是否能解决问题?

面试问题是期待来自大型软件系统的 C++ 习语和设计模式的答案。

【问题讨论】:

我认为他们希望您使用状态设计模式... 【参考方案1】:

考虑使用表而不是 switch 语句。一列可以是转换标准,另一列是目标状态。

这可以很好地扩展,因为您不必更改表处理功能;只需在表格中添加另一行。

+------------------+---------------------+---------------+
| Current state ID | transition criteria | Next state ID |
+------------------+---------------------+---------------+
|                  |                     |               |
+------------------+---------------------+---------------+

在我的工作代码中,我们使用一列函数指针而不是“下一个状态 ID”。该表是一个单独的文件,其中定义了访问器函数。有一个或多个包含语句来解析每个函数指针。

编辑 1:单独表格文件的示例。

table.h

#ifndef TABLE_H
#define TABLE_H

struct Table_Entry

    unsigned int  current_state_id;
    unsigned char transition_letter;
    unsigned int  next_state_id;
;

Table_Entry const *    table_begin(void);
Table_Entry const *    table_end(void);

#endif // TABLE_H

table.cpp:

#include "table.h"

static const Table_Entry    my_table[] =

    //  Current   Transition     Next
    //  State ID    Letter     State ID
        0,          'A',        1, // From 0 goto 1 if letter is 'A'.
        0,          'B',        2, // From 0 goto 2 if letter is 'B'.
        0,          'C',        3, // From 0 goto 3 if letter is 'C'.
        1,          'A',        1, // From 1 goto 1 if letter is 'A'.
        1,          'B',        3, // From 1 goto 3 if letter is 'B'.
        1,          'C',        0, // From 1 goto 0 if letter is 'C'.
;

static const unsigned int  TABLE_SIZE =  
    sizeof(my_table) / sizeof(my_table[0]);


Table_Entry const *
table_begin(void)

    return &my_table[0];



Table_Entry const *
table_end(void)

    return &my_table[TABLE_SIZE];
  

state_machine.cpp

#include "table.h"
#include <iostream>

using namespace std;  // Because I'm lazy.

void
Execute_State_Machine(void)

    unsigned int current_state = 0;
    while (1)
    
        char transition_letter;
        cout << "Current state: " << current_state << "\n";
        cout << "Enter transition letter: ";
        cin >> transition_letter;
        cin.ignore(1000, '\n'); /* Eat up the '\n' still in the input stream */
        Table_Entry const *  p_entry = table_begin();
        Table_Entry const * const  p_table_end =  table_end();
        bool state_found = false;
        while ((!state_found) && (p_entry != p_table_end))
        
            if (p_entry->current_state_id == current_state)
            
                if (p_entry->transition_letter == transition_letter)
                
                    cout << "State found, transitioning"
                         << " from state " << current_state
                         << ", to state " << p_entry->next_state_id
                         << "\n";
                    current_state = p_entry->next_state_id;
                    state_found = true;
                    break;
                
             
             ++p_entry;
         
         if (!state_found)
         
             cerr << "Transition letter not found, current state not changed.\n";
         
    

【讨论】:

请您提供更多详细信息,说明您将如何使用访问器函数在单独的文件中对该表进行编码。如果可能,通过一个小例子状态机:3个状态(A,B,C); A(), B(), C() 是函数,每个函数都有需要完成的操作。 这是一个使用 I/O 流的 C 状态机,而不是 C++ 状态机。 @Sanhadrin:为什么不是 C++?我承认它不是面向对象的,但它与C 和C++ 兼容。是因为我没有使用函数对象,还是std::map 结构?我明确使用了一个表,因为该表可以编译为静态数据,而不必像 std::map 那样初始化。 我花了很多时间尝试各种类型的状态机,但转换表几乎可以解决我遇到的所有问题。 +1 那里有一段非常好的代码! 我不同意“这不是 C++”之类的说法。抱歉,如果它使用符合标准的编译器进行编译,那么它就是 C++ 代码。故事结局。你可以说它不是 OO,但 C++ 的美妙之处在于它不会强迫任何一种范式。【参考方案2】:

我不知道这是否会让你通过面试,但我个人会避免手动编写任何状态机,尤其是在专业环境中。状态机是一个经过充分研究的问题,并且存在经过良好测试的开源工具,这些工具通常可以生成比您自己手动生成的代码更好的代码,它们还可以帮助您诊断状态机的问题,例如。能够自动生成状态图。

我解决这类问题的 goto 工具是:

Ragel SMC

【讨论】:

你也试过stateforge吗?它现在是开源的。免责声明,我是作者。 SMC 上面的链接应该是https://sourceforge.net/projects/smc/ 我很抱歉;当我点击它时,原始答案 SMC 链接似乎已失效。现在可以使用了。【参考方案3】:

我曾经用 C++ 编写过一个状态机,我需要对很多状态对(源 → 目标对)进行相同的转换。我想举例说明:

4 -> 8   \
5 -> 9    \_ action1()
6 -> 10   /
7 -> 11  /

8 -> 4   \
9 -> 5    \_ action2()
10 -> 6   /
11 -> 7  /

我想出的是一组(转换标准+下一个状态+要调用的“动作”函数)。为了保持一般性,转换条件和下一个状态都写成函子(lambda 函数):

typedef std::function<bool(int)> TransitionCriteria;
typedef std::function<int(int)>  TransitionNewState;
typedef std::function<void(int)> TransitionAction;   // gets passed the old state

如果您有很多转换应用到上面示例中的很多不同状态,则此解决方案很好。但是,对于每个“步骤”,这种方法需要线性扫描所有不同转换的列表。

对于上面的例子,会有两个这样的转换:

struct Transition 
    TransitionCriteria criteria;
    TransitionNewState newState;
    TransitionAction action;

    Transition(TransitionCriteria c, TransitionNewState n, TransitionAction a)
        : criteria(c), newState(n), action(a) 
;
std::vector<Transition> transitions;

transitions.push_back(Transition(
    [](int oldState) return oldState >= 4 && oldState < 8; ,
    [](int oldState) return oldState + 4; ,
    [](int oldState) std::cout << "action1" << std::endl; 
));
transitions.push_back(Transition(
    [](int oldState) return oldState >= 8 && oldState < 12; ,
    [](int oldState) return oldState - 4; ,
    [](int oldState) std::cout << "action2" << std::endl; 
));

【讨论】:

【参考方案4】:

我正在考虑一种更面向对象的方法,使用State Pattern

机器:

// machine.h
#pragma once

#include "MachineStates.h"

class AbstractState;

class Machine 
  friend class AbstractState;

public:
  Machine(unsigned int _stock);
  void sell(unsigned int quantity);
  void refill(unsigned int quantity);
  unsigned int getStock();
  ~Machine();

private:
  unsigned int stock;
  AbstractState *state;
;


// --------

// machine.cpp
#include "Machine.h"
#include "MachineStates.h"

Machine::Machine(unsigned int _stock) 
  stock = _stock;
  state = _stock > 0 ? static_cast<AbstractState *>(new Normal())
                    : static_cast<AbstractState *>(new SoldOut());


Machine::~Machine()  delete state; 

void Machine::sell(unsigned int quantity)  state->sell(*this, quantity); 

void Machine::refill(unsigned int quantity)  state->refill(*this, quantity); 

unsigned int Machine::getStock()  return stock; 

美国:

// MachineStates.h
#pragma once

#include "Machine.h"
#include <exception>
#include <stdexcept>

class Machine;

class AbstractState 
public:
  virtual void sell(Machine &machine, unsigned int quantity) = 0;
  virtual void refill(Machine &machine, unsigned int quantity) = 0;
  virtual ~AbstractState();

protected:
  void setState(Machine &machine, AbstractState *st);
  void updateStock(Machine &machine, unsigned int quantity);
;

class Normal : public AbstractState 
public:
  virtual void sell(Machine &machine, unsigned int quantity);
  virtual void refill(Machine &machine, unsigned int quantity);
  virtual ~Normal();
;

class SoldOut : public AbstractState 
public:
  virtual void sell(Machine &machine, unsigned int quantity);
  virtual void refill(Machine &machine, unsigned int quantity);
  virtual ~SoldOut();
;

// --------

// MachineStates.cpp
#include "MachineStates.h"

AbstractState::~AbstractState() 

void AbstractState::setState(Machine &machine, AbstractState *state) 
  AbstractState *aux = machine.state;
  machine.state = state;
  delete aux;


void AbstractState::updateStock(Machine &machine, unsigned int quantity) 
  machine.stock = quantity;


Normal::~Normal() 

void Normal::sell(Machine &machine, unsigned int quantity) 
  unsigned int currStock = machine.getStock();
  if (currStock < quantity) 
    throw std::runtime_error("Not enough stock");
  

  updateStock(machine, currStock - quantity);

  if (machine.getStock() == 0) 
    setState(machine, new SoldOut());
  


void Normal::refill(Machine &machine, unsigned int quantity) 
  int currStock = machine.getStock();
  updateStock(machine, currStock + quantity);


SoldOut::~SoldOut() 

void SoldOut::sell(Machine &machine, unsigned int quantity) 
  throw std::runtime_error("Sold out!");


void SoldOut::refill(Machine &machine, unsigned int quantity) 
  updateStock(machine, quantity);
  setState(machine, new Normal());

我不习惯用 C++ 编程,但这段代码显然是针对 GCC 4.8.2 clang@11.0.0 编译的,而且 Valgrind 没有显示任何泄漏,所以我想这很好。我不是在计算金钱,但我不需要这个来向你展示这个想法。

测试它:

// main.cpp
#include "Machine.h"
#include "MachineStates.h"
#include <iostream>
#include <stdexcept>

int main() 
  Machine m(10), m2(0);

  m.sell(10);
  std::cout << "m: "
            << "Sold 10 items" << std::endl;

  try 
    m.sell(1);
   catch (std::exception &e) 
    std::cerr << "m: " << e.what() << std::endl;
  

  m.refill(20);
  std::cout << "m: "
            << "Refilled 20 items" << std::endl;

  m.sell(10);
  std::cout << "m: "
            << "Sold 10 items" << std::endl;
  std::cout << "m: "
            << "Remaining " << m.getStock() << " items" << std::endl;

  m.sell(5);
  std::cout << "m: "
            << "Sold 5 items" << std::endl;
  std::cout << "m: "
            << "Remaining " << m.getStock() << " items" << std::endl;

  try 
    m.sell(10);
   catch (std::exception &e) 
    std::cerr << "m: " << e.what() << std::endl;
  

  try 
    m2.sell(1);
   catch (std::exception &e) 
    std::cerr << "m2: " << e.what() << std::endl;
  

  return 0;

一点点Makefile

CC = clang++
CFLAGS = -g -Wall -std=c++17

main: main.o Machine.o MachineStates.o
    $(CC) $(CFLAGS) -o main main.o Machine.o MachineStates.o

main.o: main.cpp Machine.h MachineStates.h
    $(CC) $(CFLAGS) -c main.cpp

Machine.o: Machine.h MachineStates.h

MachineStates.o: Machine.h MachineStates.h

clean:
    $(RM) main

然后运行:

make main
./main

输出是:

m: Sold 10 items
m: Sold out!
m: Refilled 20 items
m: Sold 10 items
m: Remaining 10 items
m: Sold 5 items
m: Remaining 5 items
m: Not enough stock
m2: Not enough stock

现在,如果你想添加一个Broken 状态,你只需要另一个AbstractState 子:

diff --git a/Machine.cpp b/Machine.cpp
index 935d654..6c1f421 100644
--- a/Machine.cpp
+++ b/Machine.cpp
@@ -13,4 +13,8 @@ void Machine::sell(unsigned int quantity)  state->sell(*this, quantity); 
 
 void Machine::refill(unsigned int quantity)  state->refill(*this, quantity); 
 
+void Machine::damage()  state->damage(*this); 
+
+void Machine::fix()  state->fix(*this); 
+
 unsigned int Machine::getStock()  return stock; 
diff --git a/Machine.h b/Machine.h
index aa983d0..706dde2 100644
--- a/Machine.h
+++ b/Machine.h
@@ -12,6 +12,8 @@ public:
   Machine(unsigned int _stock);
   void sell(unsigned int quantity);
   void refill(unsigned int quantity);
+  void damage();
+  void fix();
   unsigned int getStock();
   ~Machine();
 
diff --git a/MachineStates.cpp b/MachineStates.cpp
index 9656783..d35a53d 100644
--- a/MachineStates.cpp
+++ b/MachineStates.cpp
@@ -13,6 +13,16 @@ void AbstractState::updateStock(Machine &machine, unsigned int quantity) 
   machine.stock = quantity;
 
 
+void AbstractState::damage(Machine &machine) 
+  setState(machine, new Broken());
+;
+
+void AbstractState::fix(Machine &machine) 
+  setState(machine, machine.stock > 0
+                        ? static_cast<AbstractState *>(new Normal())
+                        : static_cast<AbstractState *>(new SoldOut()));
+;
+
 Normal::~Normal() 
 
 void Normal::sell(Machine &machine, unsigned int quantity) 
@@ -33,6 +43,10 @@ void Normal::refill(Machine &machine, unsigned int quantity) 
   updateStock(machine, currStock + quantity);
 
 
+void Normal::fix(Machine &machine) 
+  throw std::runtime_error("If it ain't broke, don't fix it!");
+;
+
 SoldOut::~SoldOut() 
 
 void SoldOut::sell(Machine &machine, unsigned int quantity) 
@@ -43,3 +57,17 @@ void SoldOut::refill(Machine &machine, unsigned int quantity) 
   updateStock(machine, quantity);
   setState(machine, new Normal());
 
+
+void SoldOut::fix(Machine &machine) 
+  throw std::runtime_error("If it ain't broke, don't fix it!");
+;
+
+Broken::~Broken() 
+
+void Broken::sell(Machine &machine, unsigned int quantity) 
+  throw std::runtime_error("Machine is broken! Fix it before sell");
+
+
+void Broken::refill(Machine &machine, unsigned int quantity) 
+  throw std::runtime_error("Machine is broken! Fix it before refill");
+
diff --git a/MachineStates.h b/MachineStates.h
index b117d3c..3921d35 100644
--- a/MachineStates.h
+++ b/MachineStates.h
@@ -11,6 +11,8 @@ class AbstractState 
 public:
   virtual void sell(Machine &machine, unsigned int quantity) = 0;
   virtual void refill(Machine &machine, unsigned int quantity) = 0;
+  virtual void damage(Machine &machine);
+  virtual void fix(Machine &machine);
   virtual ~AbstractState();
 
 protected:
@@ -22,6 +24,7 @@ class Normal : public AbstractState 
 public:
   virtual void sell(Machine &machine, unsigned int quantity);
   virtual void refill(Machine &machine, unsigned int quantity);
+  virtual void fix(Machine &machine);
   virtual ~Normal();
 ;
 
@@ -29,5 +32,13 @@ class SoldOut : public AbstractState 
 public:
   virtual void sell(Machine &machine, unsigned int quantity);
   virtual void refill(Machine &machine, unsigned int quantity);
+  virtual void fix(Machine &machine);
   virtual ~SoldOut();
 ;
+
+class Broken : public AbstractState 
+public:
+  virtual void sell(Machine &machine, unsigned int quantity);
+  virtual void refill(Machine &machine, unsigned int quantity);
+  virtual ~Broken();
+;
diff --git a/main b/main
index 26915c2..de2c3e5 100755
Binary files a/main and b/main differ
diff --git a/main.cpp b/main.cpp
index 8c57fed..82ea0bf 100644
--- a/main.cpp
+++ b/main.cpp
@@ -39,11 +39,34 @@ int main() 
     std::cerr << "m: " << e.what() << std::endl;
   
 
+  m.damage();
+  std::cout << "m: "
+            << "Machine is broken" << std::endl;
+  m.fix();
+  std::cout << "m: "
+            << "Fixed! In stock: " << m.getStock() << " items" << std::endl;
+
   try 
     m2.sell(1);
    catch (std::exception &e) 
     std::cerr << "m2: " << e.what() << std::endl;
   
 
+  try 
+    m2.fix();
+   catch (std::exception &e) 
+    std::cerr << "m2: " << e.what() << std::endl;
+  
+
+  m2.damage();
+  std::cout << "m2: "
+            << "Machine is broken" << std::endl;
+
+  try 
+    m2.refill(10);
+   catch (std::exception &e) 
+    std::cerr << "m2: " << e.what() << std::endl;
+  
+
   return 0;
 

要添加更多产品,您必须有产品地图及其各自的库存数量等等......

最终代码可以在this repo找到。

【讨论】:

另外机器应该根据数量初始化为NormalSoldOut状态,而不是默认Normal 值得注意的是,如果您尝试在 Visual Studio 2017 中使用 C++17 进行编译,则会产生错误 C2446 ":": no conversion from 'SoldOut*' to Normal*'。如果你去掉Machine的构造函数中的三元组并简单地在构造函数的主体中初始化mState变量(或者在Machine的头部初始化它)就可以了。 @micka190 好吧,这似乎很奇怪。我不专业地使用 C++,但据我了解,因为 mState 的类型是 AbstractState 并且 NormalSoldOut 都扩展了它,所以应该没有问题。关于您在头文件中初始化它的建议,如果您能指出我的代码 sn-p,我很乐意更新答案。 @HenriqueBarcelos,我只是在猜测(因为它可能只是 MSVC 的事情),但我认为三元运算符要求两个结果的类型相同(无论左侧是否变量是与两者兼容的类型)。您必须在构造函数中使用if 语句才能正确初始化它。至于标头初始化,只允许我们将其显式初始化为nullptrNormalSoldOut(没有三元和if 语句,所以基本上给它一个默认状态)。 我更新了答案,代码现在应该可以正确编译了。看起来编译器在我写完初始答案后不久就更新了,现在需要使用三元运算符进行显式转换。【参考方案5】:
#include <stdio.h>
#include <iostream>

using namespace std;
class State;

enum stateON=0,OFF;
class Switch 
    private:
        State* offState;
        State* onState;
        State* currState;
    public:
        ~Switch();
        Switch();
        void SetState(int st);
        void on();
        void off();
;
class State
    public:
        State()
        virtual void on(Switch* op)
        virtual void off(Switch* op) 
;
class OnState : public State
    public:
    OnState()
        cout << "OnState State Initialized" << endl;
    
    void on(Switch* op);
    void off(Switch* op);
;
class OffState : public State
    public:
    OffState()
        cout << "OffState State Initialized" << endl;
    
    void on(Switch* op);
    void off(Switch* op);
;
Switch::Switch()
    offState = new OffState();
    onState = new OnState();
    currState=offState;

Switch::~Switch()
    if(offState != NULL)
        delete offState;
    if(onState != NULL)
        delete onState;

void Switch::SetState(int newState)
    if(newState == ON)
    
        currState = onState;
    
    else if(newState == OFF)
    
        currState = offState;
    

void Switch::on()
    currState->on(this);

void Switch::off()
    currState->off(this);

void OffState::on(Switch* op)
    cout << "State transition from OFF to ON" << endl;
    op->SetState(ON);

void OffState::off(Switch* op)
    cout << "Already in OFF state" << endl;

void OnState::on(Switch* op)
    cout << "Already in ON state" << endl;

void OnState::off(Switch* op)
    cout << "State transition from ON to OFF" << endl;
    op->SetState(OFF);

int main()
    Switch* swObj = new Switch();
    int ch;
    do
        switch(ch)
            case 1:     swObj->on();
                    break;
            case 0:     swObj->off();
                    break;
            default :   cout << "Invalid choice"<<endl;
                    break;
        
        cout << "Enter 0/1: ";
        cin >> ch;  
    while(true);`enter code here`
    delete swObj;
    return 0;

【讨论】:

【参考方案6】:

我已经使用这些方法编写了很多状态机。但是当我为 Nexus 7000(价值 117,000 美元的交换机)编写 Cisco 的收发器库时,我使用了我在 80 年代发明的方法。那是使用一个使状态机看起来更像多任务阻塞代码的宏。这些宏是为 C 编写的,但当我为 DELL 工作时,我对 C++ 做了一些小的修改。你可以在这里阅读更多信息:https://www.codeproject.com/Articles/37037/Macros-to-simulate-multi-tasking-blocking-code-at

【讨论】:

以上是关于状态机的 C++ 代码的主要内容,如果未能解决你的问题,请参考以下文章

FPGA/数字IC手撕代码4——FSM状态机的简单应用

涉及函数指针和状态机的代码解释

FSM有限状态机的实现

试试用有限状态机的思路来定义javascript组件

15.5.3 Task实现细节状态机的结构

Stateless状态机的简单应用