C語言實現有限狀態機

以下是轉載內容:

☆─────────────────────────傳說中的分隔符───────────────────────────────────────☆

來源1http://www.cnblogs.com/swingboat/archive/2005/07/27/201488.html

【轉載1有限狀態機的實現 <script type="text/javascript"></script>

 

有限狀態機(Finite State Machine或者Finite State Automata)是軟件領域中一種重要的工具,很多東西的模型實際上就是有限狀態機。

最近看了一些遊戲編程AI的材料,感覺遊戲中的AI,第一要說的就是有限狀態機來實現精靈的AI,然後纔是A*尋路,其他學術界討論比較多的神經網絡、模糊控制等問題還不是很熱。

FSM
的實現方式:
1
switch/case或者if/else
這無意是最直觀的方式,使用一堆條件判斷,會編程的人都可以做到,對簡單小巧的狀態機來說最合適,但是毫無疑問,這樣的方式比較原始,對龐大的狀態機難以維護。

2
狀態表
維護一個二維狀態表,橫座標表示當前狀態,縱座標表示輸入,表中一個元素存儲下一個狀態和對應的操作。這一招易於維護,但是運行時間和存儲空間的代價較大。

3
使用State Pattern
使用State Pattern使得代碼的維護比switch/case方式稍好,性能上也不會有很多的影響,但是也不是100%完美。不過Robert C. Martin做了兩個自動產生FSM代碼的工具,for javafor C++各一個,在http://www.objectmentor.com/resources/index上有免費下載,這個工具的輸入是純文本的狀態機描述,自動產生符合State Pattern的代碼,這樣developer的工作只需要維護狀態機的文本描述,每必要冒引入bug的風險去維護code

4
使用宏定義描述狀態機
一般來說,C++編程中應該避免使用#define,但是這主要是因爲如果用宏來定義函數的話,很容易產生這樣那樣的問題,但是巧妙的使用,還是能夠產生奇妙的效果。MFC就是使用宏定義來實現大的架構的。
在實現FSM的時候,可以把一些繁瑣無比的if/else還有花括號的組合放在宏中,這樣,在代碼中可以3)中狀態機描述文本一樣寫,通過編譯器的預編譯處理產生1)一樣的效果,我見過產生C代碼的宏,如果要產生C++代碼,己軟MFC可以,那麼理論上也是可行的。 

 

【評】:狀態表的實現方法,《C專家編程》第8章有具體說明,轉載【6

 

☆──────────────────────傳說中的分隔符──────────────────────────────☆

來源2http://hi.baidu.com/juneshine/blog/item/6bff718bd5902f13c9fc7a14.html

【轉載2】有限狀態機的c實現

2007-05-11 15:12

網絡上可以搜索到很多有限狀態機的代碼和理論分析,這兒僅僅是做一個簡單的例子,僅供入門參考。

這兒以四位密碼校驗作爲狀態機的例子,連續輸入2479就可以通過密碼測試。一個非常簡單的例子,在實際的狀態機實例中,狀態轉移表要更復雜一些,不過方式非常類似。在狀態查詢的地方可以做優化,同時對於輸入量也可以做有效性優化。具體代碼如下:

【評】:VC6下運行該程序並沒有達到目的,即連續輸入字符2479也沒有任何輸出信息,個人根據轉載第一遍文章的FSM的實現的第一種方法,見【原創之源程序】

 

☆──────────────────────傳說中的分隔符──────────────────────────────☆

來源3http://lionwq.spaces.eepw.com.cn/articles/article/item/16363

【轉載3】有限狀態機自動機

狀態圖--一個圖的數據結構!
1.while + switch;
2.
狀態機:就是指定系統的所有可能的狀態及狀態間跳轉的條件,然後設一個初始狀態輸入給這臺機器,機器就會自動運轉,或最後處於終止狀態,或在某一個狀態不斷循環。
遊戲中狀態切換是很頻繁的。 可能以前要切換狀態就是if~else,或者設標誌,但這些都不太結構化, 如果把它嚴格的設爲一種標準的狀態機,會清楚的多。

比如控制一扇門的運動, 初始時門是關的, 當有力作用在門上時, 門開始慢慢打開,力的作用完後,門漸漸停止不動, 當有反向的力時,門又漸漸關上, 知道回到初始關的狀態。 這個你會怎麼來編程實現呢。 似乎很麻煩, 的確,沒有狀態機的思想時會很煩,設很多標誌,一堆if條件。
用狀態機的話,不只是代碼更清晰, 關鍵是更符合邏輯和自然規律, 不同狀態不同處理, 滿足條件則跳轉到相關狀態。

僞碼如下:

這是一個簡單但很典型的例子, 狀態機的應用太多了。
就說一個基本遊戲的運轉: (用到了一個狀態然後還有子狀態)

某一個狀態可以包含更多的子狀態, 這樣最好是同一層次的狀態設爲一個枚舉, 並分到另一個switch處理
enum STATES{state1, state2, state3}; state2又包含若干狀態
則再定義enum SUB_STATE2{sub_state2_1, sub_state2_2, sub_state2_3,};

想很多基本的渲染效果, 如淡入淡出, 閃爍等等, 用狀態的思想會事半功倍, 思路也更清晰。

其實像Opengl, Direct3D這樣的渲染引擎本身就是狀態機, 當你設置渲染的狀態, 這臺機器就保持這個狀態進行渲染工作,如保持光照位置,保持片元顏色, 直到你再次改變它的狀態。

狀態機的應用實在太廣, 相關理論也很多, 最近上課學的隨機過程裏也講到一些, 數字電路里的時序邏輯器件也是用狀態機來描述。 這些不必多說了。

總之, 用狀態機的角度去看待問題, 往往能把比較複雜的系統分解爲能單獨處理的衆多子狀態, 處理也會得心應手。希望大家多用它, 很好的東西。


二、
推薦這個:[程序員雜誌2004.8月刊_state模式和composite模式實現的狀態機引擎]
http://www.contextfree.net/wangyw/source/oofsm.html

個人感覺狀態機的幾個不同實現階段:
1
switch/case 最原始的實現方式,是很多的c程序員習慣採用的方式。

2、查找表[狀態、事件、動作],稍微做了一點改進。有點類似MFC的雛形。

3、在以上基礎上做的一些改進或者變體。
[
比如用一個棧結構,激活的狀態位於棧頂,自動的映射事件和動作的對應,再或者通過一些巧妙的宏等手段進行包裝。但是線性結構在實際中使用比較受限、過於技巧性的宏比較難於理解...]

4、面向對象的設計下、靈活運用模式。如上面給出的鏈接。重用性和靈活性方面都有不錯的表現。沿襲類似的設計思路、根據實際開發內容進行改造後利用。

【評】:僞代碼部分,可以幫助很好的理解【轉載1】文章中敘述的FSM的實現方法1查找表[狀態、事件、動作],稍微做了一點改進。有點類似MFC的雛形類似於【轉載1】文章中敘述的FSM的實現方法2(狀態表)

 

☆──────────────────────傳說中的分隔符──────────────────────────────☆

來源4http://hi.baidu.com/yorkbluedream/blog/item/620075faa630db1ba8d31192.html

【轉載4fsm implemented in C code(FSM狀態機用C實現)

C語言實現一個狀態機,很簡單,和大家分享
這是我做畢業設計時,用nRF24L01組建了一個簡單的網絡,做的一個小的狀態機,網絡中三個節點,開始拓撲爲網狀,後來爲星型

 

【評】:很實用的一個狀態機程序

 

☆──────────────────────傳說中的分隔符──────────────────────────────☆

來源5http://redbug.21ic.org/user1/349/archives/2007/44609.html

【轉載5】狀態機的兩種寫法
                        2004/12/26  www.armecos.com  [email protected]

yy20041226-1v1

    
有限狀態機FSM思想廣泛應用於硬件控制電路設計,也是軟件上常用的一種處理方法(軟件上稱爲FMM--有限消息機)。它把複雜的控制邏輯分解成有限個穩定狀態,在每個狀態上判斷事件,變連續處理爲離散數字處理,符合計算機的工作特點。同時,因爲有限狀態機具有有限個狀態,所以可以在實際的工程上實現。但這並不意味着其只能進行有限次的處理,相反,有限狀態機是閉環系統,有限無窮,可以用有限的狀態,處理無窮的事務。
    
有限狀態機的工作原理如圖1所示,發生事件(event)後,根據當前狀態(cur_state),決定執行的動作(action),並設置下一個狀態號(nxt_state)

                         -------------
                         |           |-------->
執行動作action
     
發生事件event ----->| cur_state |
                         |           |-------->
設置下一狀態號nxt_state
                         -------------
                            
當前狀態
                      
1 有限狀態機工作原理


                               e0/a0
                              --->--
                              |    |
                   -------->----------
             e0/a0 |        |   S0   |-----
                   |    -<------------    | e1/a1
                   |    | e2/a2           V
                 ----------           ----------
                 |   S2   |-----<-----|   S1   |
                 ----------   e2/a2   ----------
                       
2 一個有限狀態機實例

              --------------------------------------------
              
當前狀態   s0        s1        s2     | 事件
              --------------------------------------------
                       a0/s0      --       a0/s0   |  e0
              --------------------------------------------
                       a1/s1      --        --     |  e1
              --------------------------------------------
                       a2/s2     a2/s2      --     |  e2
              --------------------------------------------

               
1 2狀態機實例的二維表格表示(動作/下一狀態)

    
2爲一個狀態機實例的狀態轉移圖,它的含義是:
        
s0狀態,如果發生e0事件,那麼就執行a0動作,並保持狀態不變;
                 
如果發生e1事件,那麼就執行a1動作,並將狀態轉移到s1態;
                 
如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;
        
s1狀態,如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;
        
s2狀態,如果發生e0事件,那麼就執行a0動作,並將狀態轉移到s0態;
    
有限狀態機不僅能夠用狀態轉移圖表示,還可以用二維的表格代表。一般將當前狀態號寫在橫行上,將事件寫在縱列上,如表1所示。其中“--”表示空(不執行動作,也不進行狀態轉移)“an/sn”表示執行動作an,同時將下一狀態設置爲sn。表1和圖2表示的含義是完全相同的。
    
觀察表1可知,狀態機可以用兩種方法實現:豎着寫(在狀態中判斷事件)和橫着寫(在事件中判斷狀態)。這兩種實現在本質上是完全等效的,但在實際操作中,效果卻截然不同。

==================================
豎着寫(在狀態中判斷事件)C代碼片段
==================================
    cur_state = nxt_state;
    switch(cur_state){                  //
在當前狀態中判斷事件
        case s0:                        //
s0狀態
            if(e0_event){               //
如果發生e0事件,那麼就執行a0動作,並保持狀態不變;
                
執行a0動作;
                //nxt_state = s0;       //
因爲狀態號是自身,所以可以刪除此句,以提高運行速度。
            }
            else if(e1_event){          //
如果發生e1事件,那麼就執行a1動作,並將狀態轉移到s1態;
                
執行a1動作;
                nxt_state = s1;
            }
            else if(e2_event){          //
如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;
                
執行a2動作;
                nxt_state = s2;
            }
            break;
        case s1:                        //
s1狀態
            if(e2_event){               //
如果發生e2事件,那麼就執行a2動作,並將狀態轉移到s2態;
                
執行a2動作;
                nxt_state = s2;
            }
            break;
        case s2:                        //
s2狀態
            if(e0_event){               //
如果發生e0事件,那麼就執行a0動作,並將狀態轉移到s0態;
                
執行a0動作;
                nxt_state = s0;
            }
    }

==================================
橫着寫(在事件中判斷狀態)C代碼片段
==================================
//e0
事件發生時,執行的函數
void e0_event_function(int * nxt_state)
{
    int cur_state;
    
    cur_state = *nxt_state;
    switch(cur_state){
        case s0:                        //
觀察表1,在e0事件發生時,s1處爲空
        case s2:
            
執行a0動作;
            *nxt_state = s0;
    }
}

//e1
事件發生時,執行的函數
void e1_event_function(int * nxt_state)
{
    int cur_state;
    
    cur_state = *nxt_state;
    switch(cur_state){
        case s0:                        //
觀察表1,在e1事件發生時,s1s2處爲空
            
執行a1動作;
            *nxt_state = s1;
    }
}

//e2
事件發生時,執行的函數
void e2_event_function(int * nxt_state)
{
    int cur_state;
    
    cur_state = *nxt_state;
    switch(cur_state){
        case s0:                        //
觀察表1,在e2事件發生時,s2處爲空
        case s1:
            
執行a2動作;
            *nxt_state = s2;
    }
}

    
上面橫豎兩種寫法的代碼片段,實現的功能完全相同,但是,橫着寫的效果明顯好於豎着寫的效果。理由如下:
    1
、豎着寫隱含了優先級排序(其實各個事件是同優先級的),排在前面的事件判斷將毫無疑問地優先於排在後面的事件判斷。這種if/else if寫法上的限制將破壞事件間原有的關係。而橫着寫不存在此問題。
    2
、由於處在每個狀態時的事件數目不一致,而且事件發生的時間是隨機的,無法預先確定,導致豎着寫淪落爲順序查詢方式,結構上的缺陷使得大量時間被浪費。對於橫着寫,在某個時間點,狀態是唯一確定的,在事件裏查找狀態只要使用switch語句,就能一步定位到相應的狀態,延遲時間可以預先準確估算。而且在事件發生時,調用事件函數,在函數裏查找唯一確定的狀態,並根據其執行動作和狀態轉移的思路清晰簡潔,效率高,富有美感。
    
總之,我個人認爲,在軟件裏寫狀態機,使用橫着寫的方法比較妥帖。
    
    
豎着寫的方法也不是完全不能使用,在一些小項目裏,邏輯不太複雜,功能精簡,同時爲了節約內存耗費,豎着寫的方法也不失爲一種合適的選擇。
    
FPGA類硬件設計中,以狀態爲中心實現控制電路狀態機(豎着寫)似乎是唯一的選擇,因爲硬件不太可能靠事件驅動(橫着寫)。不過,在FPGA裏有一個全局時鐘,在每次上升沿時進行狀態切換,使得豎着寫的效率並不低。雖然在硬件裏豎着寫也要使用IF/ELSIF這類查詢語句(VHDL開發),但他們映射到硬件上是組合邏輯,查詢只會引起門級延遲(ns量級),而且硬件是真正並行工作的,這樣豎着寫在硬件裏就沒有負面影響。因此,在硬件設計裏,使用豎着寫的方式成爲必然的選擇。這也是爲什麼很多搞硬件的工程師在設計軟件狀態機時下意識地只使用豎着寫方式的原因,蓋思維定勢使然也。

    TCP
PPP框架協議裏都使用了有限狀態機,這類軟件狀態機最好使用橫着寫的方式實現。以某TCP協議爲例,見圖3,有三種類型的事件:上層下達的命令事件;下層到達的標誌和數據的收包事件;超時定時器超時事件。
    
                    
上層命令(open,close)事件
            -----------------------------------
                    --------------------
                    |       TCP        |  <----------
超時事件timeout
                    --------------------
            -----------------------------------
                 RST/SYN/FIN/ACK/DATA
等收包事件
                    
                    
3 三大類TCP狀態機事件

    
由圖3可知,此TCP協議棧採用橫着寫方式實現,有3種事件處理函數,上層命令處理函數(tcp_close);超時事件處理函數(tmr_slow);下層收包事件處理函數(tcp_process)。值得一提的是,在收包事件函數裏,在各個狀態裏判斷RST/SYN/FIN/ACK/DATA等標誌(這些標誌類似於事件),看起來象豎着寫方式,其實,如果把包頭和數據看成一個整體,那麼,RST/SYN/FIN/ACK/DATA等標誌就不必被看成獨立的事件,而是屬於同一個收包事件裏的細節,這樣,就不會認爲在狀態裏查找事件,而是總體上看,是在收包事件裏查找狀態(橫着寫)
    
    
PPP裏更是到處都能見到橫着寫的現象,有時間的話再細說。我個人感覺在實現PPP框架協議前必須瞭解橫豎兩種寫法,而且只有使用橫着寫的方式才能比較完美地實現PPP

【評】:看不大懂,先留着,有備無患

 

☆──────────────────────傳說中的分隔符──────────────────────────────☆

來源6http://blog.csdn.net/imj060336/archive/2008/01/09/2032765.aspx

【轉載6】用C語言實現有限狀態機--讀《C專家編程》

有限狀態機(finite state machine)是一個數學概念,如果把它運用於程序中,可以發揮很大的作用。它是一種協議,用於有限數量的子程序("狀態")的發展變化。每個子程序進行一些處理並選擇下一種狀態(通常取決於下一段輸入)

有限狀態機(FSM)可以用作程序的控制結構。FSM對於那些基於輸入的在幾個不同的可選動作中進行循環的程序尤其合適。投幣售貨機就是FSM的一個好例子。另外一個你可以想到的複雜的例子就是你正在用的東西,想到了嗎?沒錯,就是操作系統。在投幣售貨機的例子中,輸入是硬幣,輸出是待售商品,售貨機有"接受硬幣""選擇商品""發送商品""找零錢"等幾種狀態。

它的基本思路是用一張表保存所有可能的狀態,並列出進入每個狀態時可能執行的所有動作,其中最後一個動作就是計算(通常在當前狀態和下一次輸入字符的基礎上,另外再經過一次表查詢)下一個應該進入的狀態。你從一個"初始狀態"開始。在這一過程中,翻譯表可能告訴你進入了一個錯誤狀態,直到到達結束狀態。

C語言中,有好幾種方法可以用來表達FSM,但它們絕大多數都是基於函數指針數組。一個函數指針數組可以像下面這樣聲明:

void (*state[MAX_STATES]) ();

如果知道了函數名,就可以像下面這樣對數組進行初始化。

extern int a(),b(),c(),d();

int (*state[]) ()={a,b,c,c};

可以通過數組中的指針來調用函數:

(*state[i]) ();

所有函數必須接受同樣的參數,並返回同種類型的返回值(除非你把數組元素做成一個聯合)。函數指針是很有趣的。注意,我們可以去掉指針形式,把上面的調用寫成:

state[i] ();

甚至

(******state[i]) ();

這是一個在ANSI C中流行的不良方法:調用函數和通過指針調用函數(或任意層次的指針間接引用)可以使用同一種語法。

如果你想幹得漂亮一點,可以讓狀態函數返回一個指向通用後續函數的指針,並把它轉換爲適當的類型。這樣,就不需要全局變量了。如果你不想搞得太花哨,可以使用一個switch語句作爲一種簡樸的狀態機,方法是賦值給控制變量並把switch語句放在循環內部。關於FSM還有最後一點需要說明:如果你的狀態函數看上去需要多個不同的參數,可以考慮使用一個參數計數器和一個字符串指針數組,就像main函數的參數一樣。我們熟悉的int argc,char *argv[]機制是非常普遍的,可以成功地應用在你所定義的函數中。 

☆──────────────────────傳說中的分隔符──────────────────────────────☆

【原創之源程序】密碼鎖(最簡單的實現switch/if-else形的)

【另】還有兩個演示程序

面向對象的:http://download.csdn.net/source/1219696

面向過程的:http://download.csdn.net/source/1219691

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章