普通的年轻状态机,纯C语言
后。然后,我們聯系了狀態機,它是在編譯原理課程。符串。
再后來。我們在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 };
2?
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>
3?
4?
5 //recorder?
6?
7 enum state { s_stop, s_play, s_forward, s_backward, s_pause, s_record ?};
8 enum message { play, stop, forward, backward, record, pause };
9?
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语言的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【Android开发日记】第一个任务An
- 下一篇: JavaWeb--数据库添加