首页 » C语言解惑 » C语言解惑全文在线阅读

《C语言解惑》第11章 状态机

关灯直达底部

状态机的设计比较复杂,这里主要是分析最简单的情况,作为入门知识的介绍。

1.状态机的条件错误

【例11.1】下面是统计输入单词的程序,但输出结果不对,请分析它的工作原理并改正错误。


#include <stdio.h>enum { OUT=0, IN=1 };int main(){     int count = 0,  state = OUT;     char c;     while((c = getchar()) != /'#/')     {        if(c==/' /' || c == /'t/' || c == /'/0/')                  state=OUT;        else if(state == OUT)              {               state=IN;               ++count;        }     }     printf(/"word count:%dn/", count);     return 0;}  

程序示范运行如下:


We are here!Go home!Right!#word count:4  

【解答】先分析最简单的情况,只输入1行文字,遇到“#”结束。

程序使用两种状态来描述,即IN和OUT。它使用状态和状态转换来描述:

(1)初始state=OUT。

(2)当读入空格时,相当于先输入空格,if条件满足,重新执行state=OUT并结束这一次的循环,开始下一次循环。如果继续是空格,则继续这个过程。

(3)由此可见,当处于OUT时,如果读入的是空格,状态不发生转换。一直到读到非空格时,执行else if语句,就置state=IN,计数器加1。

(4)当读入处于IN状态时,读到非空格时,if和else if的条件均不成立,状态不转换,读到空格时,if成立,置state=OUT,即读完一个单词。

(5)重复上述过程。

当需要输入多行时,应该将换行与空格等同看待。但/'/0/'是字符串结束符,换行符是/'n/'。因为缺少换行符,所以把换行后的第1个单词与上一行最后一个单词算作一个单词,就少计数2个,所以输出4。用n替换0即可。运行时再输入相同数据,就会得到正确结果为6。

2.遗漏状态机的状态

【例11.2】下面是增加输入状态统计输入单词的程序,但输出结果不对,请找出并改正错误。


#include <stdio.h>enum { OUT=0, IN=1 };//根据输入类型,字符返回1,其他为0//input=get_input(char c)决定input状态OUT还是INint get_input(char c){       if ( c>= /'a/' && c <= /'z/')              return IN;       if ( c>= /'A/' && c <= /'Z/')              return IN;       return OUT;}int main(){       int words=0, state=OUT, input=OUT;       char c;       while((c = getchar()) != /'#/')       {            input=get_input(c);            //起始状态为0,无输入,保持state=0            if((state==OUT)&&(input==OUT))            {                state=OUT;            }            //state=0,input=1,置state=1并计数             else if((state==OUT)&&(input==IN))            {                state=IN;                words++;            }       }       printf(/"word count:%dn/", words);       return 0;}  

运行错误结果如下:


we are here!Go home!#word count:1  

【解答】本题是与上面的例子的目的相同,都是统计输入文本的单词数。上题使用一个状态变量编程,本题使用两个状态编程。

本题设计一个函数get_input,用来将输入转换为OUT和IN,供输入状态input使用。一个状态量的取值就是2种,但两个状态量的变化却有4种。程序在处理时,只处理两种,所以程序的输出不正确。

假设输入一串文字“How are you?Fine!”,分析一下状态变化规律。为了好对照,改用0和1表示两种状态值。对input而言,约定输入字符为1,输入非字符为0。state也使用相同约定。开始状态两者均为0。


输入     How are you? Fine! input  0111011101110011110state  0111011101110011110  

因字母大小不一,所以上下对不齐,就按顺序对应,状态表中把state放在前面。

起始:input=0,state=0。如果开始时使用空格,也与此一样,状态都不发生变化,即


if((state==OUT)&&(input==OUT))    state=OUT;  

当前状态表0 0(OUT,OUT)

输入:输入有效字符input=1,state变为1,单词计数,即


else if((state==OUT)&&(input==IN)){     state=IN;     words++;}  

状态变换对应:0 1(OUT IN)

转换后状态1 1(IN,IN)

输入结束:input=0时表示结束,置state=0。程序里没处理这一项,即


else if((state==IN)&&(input==OUT))   state=OUT;  

状态变换对应:1 0(IN OUT)

转换后状态0 0(OUT,OUT)

继续输入:保持input和state都为1。程序里也没处理这一项,即


if((state==IN)&&(input==IN))  state=IN;  

由以上分析可知。需要处理所有可能的状态转换。


//改正后的程序#include <stdio.h>enum { OUT=0, IN=1 };//根据输入类型,字符返回1,其他为0//get_input函数决定input状态0还是1int get_input(char c){     if ( c>= /'a/' && c <= /'z/')            return IN;     if ( c>= /'A/' && c <= /'Z/')            return IN;     return OUT;}int main(){         int words=0, state=OUT, input=OUT;     char c;     while((c = getchar()) != /'#/')     {         input=get_input(c);          //起始状态为0,无输入,保持state=0         if((state==OUT)&&(input==OUT))               {                state=OUT;         }          //state=0,input=1,置state=1并计数          else if((state==OUT)&&(input==IN))         {                state=IN;                words++;              //input=1,state从0变到1,单词起点      }      //state=1,等待input=0,置state=0,一个单词结束      else if((state==IN)&&(input==OUT))      {           state=OUT;      }     //state=1,input=1,置state=1,继续输入     if((state==IN)&&(input==IN))     {       state=IN;     }   } printf(/"word count:%dn/", words); return 0;}  

验证如下:


we are here!Go home!#word count:5  

利用两个状态建立状态转移的方法很有用处。

3.使用状态机的例子

【例11.3】演示使用状态机对数组里的单词计数并输出单词的程序。

【分析】因为要输出单词,所以要使用计数器记录单词有多少字符以便打印。还需要设计一个字符指针跟踪字符串。为了简单,直接使用0和1表示状态。假设字符串的内容,用这个字符串画出input和state的关系,根据这些关系,注意打印是在一个单词结束之后,所以是选寻找打印的位置,然后计数,单词结束后再打印,所以p只是列出对应的打印操作,用“+”表示第1个单词字符计数。


buf      How are you? Fine! thank you.   input   011101110111001111001111101110state   011101110111001111001111101110         ppp ppp ppp  pppp  ppppp ppp         +++  

因为其他情况一样,所以只分析一下打印。打印H,state从0到1,input=1时,将buf的地址赋给指针p(p=&buf[i])。为了方便打印,将单词计数放在单词的结束,这时就可以打印遍历过的字符。即state=1,input变为0,将state置0后,打印单词。

单词的计数是在继续输入时进行的,即state=1,input=1,input继续为1。

有两个变量和4种状态,为了进一步理解它们的操作,加入一些打印信息。完成的程序和运行结果如下所示,注意标点符号不属于单词。


#include <stdio.h>int get_input(char);int main(){     char buf=/"How are you? Fine! thank you./";      int input=0, i=0, state=0, words=0;     char c;     char *p=NULL;                    //打印起点     int counter=0;               //单词计数     while(1)     {        c=buf[i];        input=get_input(c);        printf(/"c=%c,input=%d /",c,input);        if(c==/'/0/')               break;        if((state==0)&&(input==0))              {             state=0;        }        //state=0,input=1,置state=1并打印         else if((state==0)&&(input==1))        {             state=1;             p=&buf[i];               //input=1,state从0变到1,单词起点        }        //state=1,等待input=0,置state=0,一个单词结束        //输出这个单词并置计数器为0        else if((state==1)&&(input==0))        {             int j=0;             state=0;             words++;             printf(/"找到第%d个单词:/",words);             for(j=0;j<counter;j++)     //打印单词                   printf(/"%c/",p[j]);             printf(/"n/");             counter=0;               //单词计数器计数置0        }        //state=1,input=1,置state=1        //单词计数器计数        if((state==1)&&(input==1))        {             state=1;             counter++;               //单词计数器计数             printf(/"counter=%dn/",counter);        }        i++;                    //循环变量加1后返回起点     }    //循环结束    printf(/"一共找到%d个单词。n/",words);     return 0;}//字符返回1,其他为0int get_input(char c){    if ( c>= /'a/' && c <= /'z/')           return 1;    if ( c>= /'A/' && c <= /'Z/')           return 1;    return 0;}c=H,input=1 counter=1c=o,input=1 counter=2c=w,input=1 counter=3c= ,input=0 找到第1个单词:Howc=a,input=1 counter=1c=r,input=1 counter=2c=e,input=1 counter=3c= ,input=0 找到第2个单词:arec=y,input=1 counter=1c=o,input=1 counter=2c=u,input=1 counter=3c=?,input=0 找到第3个单词:youc= ,input=0 c=F,input=1 counter=1c=i,input=1 counter=2c=n,input=1 counter=3c=e,input=1 counter=4c=!,input=0 找到第4个单词:Finec= ,input=0 c=t,input=1 counter=1c=h,input=1 counter=2c=a,input=1 counter=3c=n,input=1 counter=4c=k,input=1 counter=5c= ,input=0 找到第5个单词:thankc=y,input=1 counter=1c=o,input=1 counter=2c=u,input=1 counter=3c=.,input=0 找到第6个单词:youc= ,input=0 一共找到6个单词。 

这个程序的state和input的状态都是两个,用上面的程序也能去掉空格,但是不能输出标点符号。

【例11.4】修改上述程序使其去掉字符串中的多余空格。


#include <stdio.h>int get_input(char);int main(){    char buf=/"How    are you? Fine! thank    you./";     int input=0, i=0, state=0;    char c;    char *p=NULL;               //打印起点    int counter=0;               //单词计数    while(1)    {        c=buf[i];        input=get_input(c);        if(c==/'/0/')               break;        if((state==0)&&(input==0))              {             state=0;        }        else if((state==0)&&(input==1))        {             state=1;             p=&buf[i];          //input=1,state从0变到1,单词起点        }        else if((state==1)&&(input==0))        {             int j=0;             state=0;             words++;             for(j=0;j<counter;j++)                   printf(/"%c/",p[j]);             printf(/" /");             counter=0;        }        if((state==1)&&(input==1))        {              state=1;              counter++;        }        i++;    }    printf(/"n/");    return 0;}int get_input(char c){    if ( c>= /'a/' && c <= /'z/')           return 1;    if ( c>= /'A/' && c <= /'Z/')           return 1;    return 0;}  

程序运行结果如下:


How are you Fine thank you  

为了解决这个问题,可以使state和input的状态数目不一样。

【例11.5】使用状态机去除多余的空格的程序。

第1个空格是重要的,状态要发生改变。这里把input空格定义为1,其他应为0。

对state而言,关心的是空格,所以字符对应0。第1个空格为1,第2个空格就必须与之区分,定义为2。同理,第3个空格也应为2。对字符不关心,遇到字符(包括标点符号)回到0状态,只要不是状态2,就都打印出来。


buf   /"   How are   you?   Fine!  thank you.n  input   1100010001110000111000001100000100000state  01200010001220000122000001200000100000 p      p pppppppp  ppppp  pppppp ppppppppppp  

对照上述状态,可以列出一个状态跳转表。


0 0-->0   0 1-->1   1 0-->0  1 1-->2   2 0-->0   2 1-->2  

根据状态跳转表,编写出如下程序。


#include <stdio.h>int get_input(char);int main(){        char buf=/"How are     you? Fine! thank you./";     int input=0, i=0, state=0;    char c;    char *p=NULL;          //打印起点    int counter=0;          //单词计数    while(1)    {        c=buf[i];        input=get_input(c);        if(c==/'/0/') break;        if((state==0)&&(input==0))        {              state=0;              printf(/"%c/",c);        }        else if((state==0)&&(input==1))        {              state=1;              printf(/"%c/",c);        }        else if((state==1)&&(input==0))        {              state=0;              printf(/"%c/",c);       }        else if((state==1)&&(input==1))       {              state=2;              //nothing       }        else if((state==2)&&(input==0))        {              state=0;              printf(/"%c/",c);        }        if((state==2)&&(input==1))        {              state=2;              //no out;        }       i++;    }     printf(/"n/");     return 0;}int get_input(char c){    if ( c== /' /')           return 1;    return 0;}  

程序运行结果如下:


How are you? Fine! thank you.  

将程序修改一下,即可用于滤除文件中的多余空格。