我们第一次接触到了状态机。在数字电路课程。计数器、串行奇偶校验、考了1连续报错电路 等待,两者都需要一个状态机模型。电路实现这些功能,与状态机的状态转移图、状态转移表是等价。



后。然后,我们联系了状态机,它是在编译原理课程。符串。

再后来。我们在GUI界面设计中,须要设置一些控件在某些条件下 禁用,某些条件下使能,某些条件下打个对号。这也能够用状态机模型来控制。



1. 不要写成 消息响应/事件处理



状态机和消息响应都是 双层 switch-case 结构。不同的是,状态机的外层是状态,内层是消息。消息响应外层是消息,内层是状态。



有的同学会说。那又有多大的差别呢?代码仅仅是外在形式而非本质,它所反应的是你对模型的理解,或者说。对于问题,你使用了哪种模型。



消息响应适合于这种情形:有非常多种消息,对于同一种消息,你的程序总是给出同一种反应。打个例如。你女朋友喜欢吃冰淇淋,不论什么时候你给她买,她都高兴,或者转怒为喜,或者转悲为喜,总之,会置心情为"喜"。这种情形,适合用消息响应解决。

而状态机适合于还有一种情形,你的程序是"有状态的",它在不同的情况 (状态)下,会对同一消息做出不同的反应。状态,是一种数据。可是它影响流程的行为。

按面向对象的观点。数据与流程间的这样的高内聚关系,很适合用 类 来实现。

这是题外话。我们回到女朋友和冰淇淋间的关系。你女朋友可能并不是在不论什么情况下吃了冰淇淋都高兴,比方刚刚吃完十个八个的时候...这与她当前的状态有关。



状态机中,我们须要掌握的核心的数据是:当前状态,当前消息,将迁移到的状态,在迁移中发生的动作。



在状态机代码之前,请先看一段消息响应机制。VC生成的win32api代码大抵如此。我们随便找来一段片断看看:



1 LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

2 {

3 int wmId, wmEvent;

4 PAINTSTRUCT ps;

5 HDC hdc;

6 switch (message)

7 {

8 case WM_COMMAND:

9 wmId    = LOWORD(wParam);

10 wmEvent = HIWORD(wParam);

11 case ID_MENU_GO: .... break;

12 case IDM_ABOUT:  .... break;

13 case IDM_EXIT:   .... break;

14 default:

15 return DefWindowProc(hWnd, message, wParam, lParam);

16 }

17 break;

18 case WM_PAINT: .... break;

19 case WM_DESTROY: ... break;

20 case WM_KEYDOWN: ... break; 

21 default: return DefWindowProc(hWnd, message, wParam, lParam);

22 }

23 return 0;

24 }



第6行開始到第22行结束,对每一个消息给出一个响应。没错,win32api也把这个传进来的东西称为 message。

这是非常典型的适合消息响应机制的情形。程序对于同样的消息,处理的方法总是同样的。



我们经常错误地把状态机写成了消息响应。消息这部分处理得不错,可是。因为没有非常好地记录和迁移状态,写起来easy把自己写糊涂了。

无他,用错了工具。拿螺丝刀打孔,不是工具差,而是project师选错了工具。

2. 状态机实例。录音机



实例得是相对简单的,不然我们非常easy淹没在细节之中,没有足够精力去关注状态机本身的机制了。如果我们仿真一台录音机...



我们先如果你见过录音机。录音机是一种以前先进的设备。有一个或两个"卡",能够放进磁带。

"卡"前面有几个按键,这几个按键上的标识由于图形简单且示意性强,如今还在广泛使用。它们各自是 播放 > 、暂停 || 、快进 >> 、快退<< 、录音 O 、停止 []。

这几个按键之间是有一定的"相互排斥关系"的。比方当播放键按下时,我们不应该能把 快进键按下。当然,淘气的同学可能这样干过,我们会听到"咔咔"的声音,然后是家长骂败家玩艺的声音。能够就"相互排斥关系"開始敲代码。可是我认为这样有点麻烦。



我们觉得,这种"相互排斥关系"是由于录音机是"有状态的"。

所以。我们打算用状态机来实现。状态转换图是这种。请读图的时候关注这四点:当前状态。当前消息,将迁移到的状态,在迁移中发生的动作 (本例中没有) 。







备注:我实在想不起来 暂停 和 停止 之间的关系了。似乎是这种。又似乎不是。反正大概是那么个意思,不影响对状态机的理解。就这么地吧。

接下来是C代码实现。

3. 接口 及 測试



看到下面代码,有的同学会说,你这不就是主程序么。为什么要把小标题叫做接口。

由于,它规定了我们的状态机函数将是什么样子的。



1 enum message { play, stop, forward, backward, record, pause };



3 int main(int argc, char *argv[])

4 {

5     char c=0x00;

6     while(1)

7     {

8         c = getchar();

9         switch(c)

10         {

11             case ' ': state_change(pause); break;

12             case 'p':  state_change(play); break;

13             case 'r': state_change(record); break;

14             case 's': state_change(stop); break;

15             case 'f': state_change(forward); break;

16             case 'b': state_change(backward); break;

17             case 'q':     return EXIT_SUCCESS;

18         }

19     }

20     return EXIT_SUCCESS;

21 }



上述代码规定了。状态机迁移函数的原型/签名是 void state_change(enum

message m)。

測试的时候,我们这样做:./state < test.in。

test.in的内容是"psfsbspq"。測试时期待看到输出的状态迁移过程。之所以这样做,而不是每次从控制台手动输入。是由于每次測试的内容都应该是同样的--同样的输入,程序有同样的反应--可重现性。或者说,DRY原则。



一个很值得我们注意的问题。在上述接口中。我们看不到"状态"。其实,我们将会定义:



enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record };



可是,接口以外的代码,是 *不应该* (是不应该。不是 不必要,是一定不要) 知道状态的,既不应该知道当前状态,也不应该知道将要迁移到哪个状态。也不应该知道在迁移过程中应该做什么动作。

假设接口以外的代码知道了这些,就侵入了状态机的隐私。子系统的边界就模糊了。而契约的首要任务就是规定边界。规定国家与个人、个人与个人、个人与集体的边界。

这一原则,早在195X年,软件project刚刚開始的时候就确立了,是最初确立的原则,即 信息隐藏。

后面的原则,都是它的儿子孙子。

有个比喻讲过这个道理。当你在超市出口付款的时候。你会自己把钱从钱夹里拿出来递给售货员,而不会转过身去对她说,"在我屁股兜里,你自己掏吧。别忘了把零钱放回来。"这既添加了如果--你极端信任她。也添加了她的责任。



接口,最基本的任务就是为了明白责任,把责任分布在子系统边界两側。其次才是规定调用的方法,即边界长什么样。



4. 状态迁移



下面是状态机的代码片断。



1 enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record};

2 void state_change(enum message m)

3 {

4  static enum state s=s_stop;

5  switch (s)

6  {

7 case s_play:

8     if(m==stop)

9        {

10 s = s_stop;

11 printf("stop.\n");        

12        }

13        else if (m==pause)

14        {

15              s = s_pause;

16 printf("pause");

17        }

18        break;



我们还是要关注那四个关键点: (1) 当前状态, (2) 当前消息, (3) 将迁移到哪个状态, (4) 迁移中会做哪些动作。

(1) 当前状态必定是第1行的枚举类型中的一个。我们初始化状态为 停止,见第4行。

在第5行到第7行,我们的双重 switch-case 的外层 按当前状态分类。例如以下。

5  switch (s)

6  {

7 case s_play:

以下还有非常多 case,第1行的枚举类型中的每个状态,都有一个 case。

(2) 当前消息。

假设当前状态是第7行了,那么,当前消息由双层 switch-case的内层,即第8行。第13行的 if...else if 来响应。



(3) 将迁移到哪个状态。

在 s_play状态 (第7行) 接收到 stop 消息 (第8行)的话,将迁移到 s_stop 状态,即第10行。



(4) 在迁移中会做哪些动作,假设还是这个状态这个消息,会做的动作是 第11行。打印一段文字描写叙述接下来的状态。



在函数 void state_change(enum message m) 中,维护了当前状态。规定了在某种状态下-接收到某个消息,会迁移到哪个状态。在状态迁移中做哪些动作。



主函数在调用state_change时,是通过这一接口,向状态机发送一个消息。由状态机对这个消息做出适合自己当前状态的响应--状态迁移、动作。主函数所示,是一个多彩或善变的女人。而她之所以对同一消息做出不同响应的原因,在她的内心深入保留着,那是她不会对你说的状态。以及状态迁移中的波澜壮阔。即使表面上善变的状态机,也是能够理解和预測的,假设她对你倘开心扉。同意你一行一行把附录A中的代码读完,了解全部的 switch-case,了解全部的状态下她将会怎样响应每一种消息。



附录A 完整代码





1 #include <stdlib.h>

2 #include <stdio.h>





5 //recorder 



7 enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record  };

8 enum message { play, stop, forward, backward, record, pause };



10 

11 void state_change(enum message m)

12 {

13  static enum state s=s_stop;

14  switch (s)

15  {

16 case s_play:

17     if(m==stop)

18        {

19 s = s_stop;

20    printf("stop.\n");        

21        }

22        else if (m==pause)

23        {

24             s = s_pause;

25 printf("pause");

26        }

27        break;

28 case s_pause:

29 if(m==pause)

30 {

31 s = s_play;

32 printf("play.\n");        

33 }

34 else if(m==stop)

35 {

36 s = s_stop;

37 printf("stop.\n");        

38 }

39 break;

40     case s_stop:

41 if(m==play)

42 {

43 s = s_play;

44 printf("play.\n");        

45 }

46 if(m==backward)

47 {

48 s = s_backward;

49 printf("backward.\n");        

50 }

51 if(m==forward)

52 {

53 s = s_forward;

54 printf("forward.\n");        

55 }

56 if(m==record)

57 {

58 s = s_record;

59 printf("record.\n");        

60 }

61 break;

62 case s_forward:

63 if(m==stop)

64 {

65 s = s_stop;

66 printf("stop.\n"); 

67 }

68 break;

69 case s_backward:

70 if(m==stop)

71 {

72 s = s_stop;

73 printf("stop.\n"); 

74 }

75 break;

76 case s_record:

77 if(m==stop)

78 {

79 s = s_stop;

80 printf("stop.\n"); 

81 }

82 break;

83

84         

85  }

86      

87 }

88 

89 

90 int main(int argc, char *argv[])

91 {

92     char c=0x00;

93     while(1)

94     {

95         c = getchar();

96         switch(c)

97         {

98             case ' ': state_change(pause); break;

99             case 'p':  state_change(play); break;

100             case 'r': state_change(record); break;

101             case 's': state_change(stop); break;

102             case 'f': state_change(forward); break;

103             case 'b': state_change(backward); break;

104             case 'q':     return EXIT_SUCCESS;

105         }

106 

107         

108     }

109     

110     return EXIT_SUCCESS;

111 }





附录B 状态图源码 in graphviz





digraph state

{

graph [ nodesep=1.2];

rankdir = LR;



播放 -> 暂停 [label="按下 || "];

暂停 -> 播放 [label="按下 || "];

暂停 -> 停止 [label="按下 []"];

停止 -> 播放 [label="按下 >"];

播放 -> 停止 [label="按下 []"];

停止 -> 快退 [label="按下 <<"];

停止 -> 快进 [label="按下 >>"];

快进 -> 停止 [label="按下 []"];

快退 -> 停止 [label="按下 []"];

停止 -> 录音 [label="按下 O"];

录音 -> 停止 [label="按下 []"];





}













--------------------





博客会手工同步到下面地址:





[http://giftdotyoung.blogspot.com]





[http://blog.csdn.net/younggift]

=======================

版权声明:本文博客原创文章。博客,未经同意,不得转载。

普通的年轻状态机,纯C语言的更多相关文章

  1. geek青年的状态机,查表,纯C语言实现

    geek青年的状态机,查表,纯C语言实现 1. 问题的提出.抽象 建一,不止是他,不少人跟我讨论过这种问题:怎样才干保证在需求变更.扩充的情况下.程序的主体部分不动呢? 这是一个很深刻和艰难的问题.在 ...

  2. 纯C语言INI文件解析

    原地址:http://blog.csdn.net/foruok/article/details/17715969 在一个跨平台( Android .Windows.Linux )项目中配置文件用 IN ...

  3. 经典数独游戏+数独求解器—纯C语言实现

    "心常乐数独小游戏"(下面简称"本软件")是一款windows平台下的数独游戏软件. 本软件是开源.免费软件. 本软件使用纯C语言编写,MinGW编译,NSIS ...

  4. 不好意思啊,我上周到今天不到10天时间,用纯C语言写了一个小站!想拍砖的就赶紧拿出来拍啊

    花10天时间用C语言做了个小站 http://tieba.yunxunmi.com/index.html 简称: 云贴吧 不好意思啊,我上周到今天不到10天时间,用纯C语言写了一个小站!想拍砖的就赶紧 ...

  5. FastDFS是纯C语言实现,只支持Linux,适合以中小文件为载体的在线服务,还可以冗余备份和负载均衡

    一.理论基础 FastDFS比较适合以中小文件为载体的在线服务,比如跟NGINX(APACHE)配合搭建图片服务器. 分布式文件系统FastDFS FastDFS是纯C语言实现,只支持Linux.Fr ...

  6. 异想家纯C语言矩阵运算库

    Sandeepin最近做的项目中需要在嵌入式芯片里跑一些算法,而这些单片机性能不上不下,它能跑些简单的程序,但又还没到上Linux系统的地步.所以只好用C语言写一些在高级语言里一个函数就解决的算法了, ...

  7. 状态机的c语言编程

    http://blog.csdn.net/shandongdaya/article/details/7282547 一 有限状态机的实现方式 有限状态机(Finite State Machine或者F ...

  8. 关于linux内核用纯c语言编写的思考

    在阅读linux2.6 版本内核的虚拟文件系统和驱动子系统的时候,我发现内核纯用c语言编写其实也是有一点不方便,特别是内核中大量存在了对象的概念,比如说文件对象,描述起来使用对象描述,但是对象在c语言 ...

  9. <Win32_20>纯c语言版的打飞机游戏出炉了^_^

    经过昨天的苦战,终于完成了纯C版的打飞机游戏——使用微信打飞机游戏的素材,不过玩法有些不同,下面会有详述 一.概述游戏的玩法.实现效果 1. 游戏第一步,简单判断一下,给你一个准备的时间: 2.选择& ...

随机推荐

  1. 与众不同 windows phone (2) - Control(控件)

    原文:与众不同 windows phone (2) - Control(控件) [索引页][源码下载] 与众不同 windows phone (2) - Control(控件) 作者:webabcd介 ...

  2. windows系统port监听

    通常情况下.假设想发现全部已经使用的和正在监听的port,我们能够使用netstat命令. netstat并不是一个port扫描工具.假设你想扫描计算机开放了哪些port的话.建议使用本文介绍的方法. ...

  3. JVM查找类文件的顺序(转)

    配置classpath 根据path环境变量的原理,可以定义一个名为classpath环境变量,将要运行的class文件所在目录定义在该变量中. 例:set classpath=c:\ classpa ...

  4. Androidproject夹

    创建一个Android应用 File -> New -> Android Application Project 填写应用名称.project名称.包名 设置project的相关信息.默认 ...

  5. Hbase集群环境搭建

    Hbase数据库依赖 Hadoop和zookeeper,所以,安装Hbase之前,需要先把zookeeper集群搭建好.(当然,Hbase有内建的zookeeper,不过不建议使用).Hbase配置上 ...

  6. Pods was rejected as an implicit dependency for &#39;libPods.a&#39; because its architectures &#39;x86_64&#39; didn

    引入cocoaPods后.第一次编译,或者运行update后 可能报这个错误: Pods was rejected as an implicit dependency for 'libPods.a' ...

  7. OCP读书笔记(3) - 使用RMAN恢复目录

    创建恢复目录 在hndx上创建恢复目录:[oracle@oracle admin]$ export ORACLE_SID=hndx[oracle@oracle admin]$ sqlplus / as ...

  8. 在 Java 项目中解压7Zip特殊压缩算法文件

    1 问题描写叙述 Java Web 后端下载了一个经特殊算法压缩的 zip 文件,由于不能採用 java 本身自带的解压方式,必须採用 7Zip 来解压.所以,提到了本文中在 java web 后端调 ...

  9. Linux内核升级

    一.测试环境 CentOS6.5 X86 64位 内核版本为 2.6.32 VM 10.07 二.编译内核版本 2.1.kernel 3.2.71 2.2.kernel 3.4.108 2.3.ker ...

  10. 从零开始,创建GitHub团队开发环境

    从零开始,创建GitHub团队开发环境 GitHub提供免费的团队环境,不过免费仓库容量是300MB,请大家注意. 申请GitHub个人账号 1. 使用浏览器访问GitHub主页.如果使用IE,尽量不 ...