查看原文
其他

这个编程技巧别说我没告诉你

守望先生 编程珠玑 2022-09-11

来源:公众号【编程珠玑】

作者:守望先生

ID:shouwangxiansheng


前言

有读者在后台留言说用c写一篇有限状态机的推文,正好之前也用过,就分享一下吧。

背景

先举一个简单的例子,假设是这样的,一个小孩有两种状态,睡眠,清醒。睡的时候可能会撒尿,微笑,撒尿之后会转为清醒状态,而清醒的时候可能会笑,会吃,吃完之后会转会睡眠状态
用C语言实现,一般写法可能是这样的:

//来源:公众号【编程珠玑】
#include <stdio.h>
enum KID_STATUS
{
    SLEEP = 0,
    WAKE = 1,
    INVALID = 2
};
enum KID_ACTION
{
    SMILE = 0,
    EAT = 1,
    PEE = 2
};
/*孩子当前状态*/
static int nowStatus = WAKE;

/*孩子行为*/
int smile(int status)
{
    printf("kid smile\n");
    return status;
}
int eat(int status)
{
    printf("kid eat\n");
    return SLEEP;
}
int pee(int status)
{
    printf("kid pee\n");
    return WAKE;
}
/*孩子产生某个行为*/
void execute(int action)
{
    int nextStatus;
    /*醒着时的行为*/
    if(WAKE == nowStatus)
    {
        switch(action)
        {
            case SMILE:
            {
                nextStatus = smile(nowStatus);
                break;
            }
            case EAT:
            {
                nextStatus = eat(nowStatus);
                break;
            }
            case PEE:
            {
                nextStatus = pee(nowStatus);
                break;
            }
            default:
                printf("unknow action when wake %d\n",action);
                nextStatus = nowStatus;
                break;
        }
    }
    /*睡着时的行为*/
    else if(SLEEP == nowStatus)
    {
        switch(action)
        {
            case SMILE:
            {
                nextStatus = smile(nowStatus);
                break;
            }
            casePEE:
            {
                nextStatus = pee(nowStatus);
                break;
            }
            default:
                printf("unknow action when sleep %d\n",action);
                nextStatus = nowStatus;
                break;
        }
    }
    else
    {
        printf("unknow action\n");
    }
    nowStatus = nextStatus;
    printf("next status is %d\n",nowStatus);
}
int main(void)
{
    nowStatus = SLEEP;
    execute(EAT);
    return 0;
}

这段代码的意图就非常明显了,首先判断当前所在状态, 然后找到其中的行为,最后执行。
但是这段代码有以下几个特点:

  • 新加一种行为需要修改execute函数

  • 新加一种行为需要增加更多分支代码

  • 新加一种状态,需要新增一个大的分支

  • 哪些状态有哪些行为不是很明显

换一种写法

在《高级指针话题-函数指针》中介绍了函数指针,以及在《编程技巧-跳转表》介绍了函数跳转表。这里我们把代码调整一下,看看结合跳转表和状态机,能写出什么样的代码。

//来源:公众号【编程珠玑】地址:https://www.yanbinghu.com
#include <stdio.h>
/*省略枚举定义和函数定义,与前面相同*/
typedef int (*Handler)(int);//函数指针
/*用于处理某种状态的行为*/
typedef struct Act_Handler
{

    int action;
    Handler handler;
}Act_Handler;
/*某种状态的处理集*/
typedef struct Stat_Handler
{

    int status;
    int actNum;
    Act_Handler *actHandler;
}Stat_Handler;
/*sleep状态行为处理*/
Act_Handler sleepHandler[] = 
{
    {SMILE,smile},
    {PEE,pee}
};
/*wake状态行为处理*/
Act_Handler weakHandler[] = 
{
    {SMILE,smile},
    {PEE,pee},
    {EAT,eat}
};
/*状态处理*/
static Stat_Handler statHandler[] = 
{
    {SLEEP,sizeof(sleepHandler)/sizeof(Act_Handler),sleepHandler},
    {WAKE,sizeof(weakHandler)/sizeof(Act_Handler),weakHandler},
};
static int statSize = sizeof(statHandler)/ sizeof(Stat_Handler);
void execute(int action)
{
    if(nowStatus >= statSize)
    {
        printf("unknow status\n");
        return;
    }
    printf("now status is %d,action %d\n",nowStatus,action);
    int nextStatus = nowStatus;
    Act_Handler *actHandler = statHandler[nowStatus].actHandler;
    int actNum = statHandler[nowStatus].actNum;
    int actIdx = 0;
    /*遍历指定状态下的行为集,找到对应的行为*/
    for(actIdx = 0;actIdx < actNum;actIdx++)
    {
        if(actHandler[actIdx].action == action)
        {
            nextStatus = (actHandler[actIdx].handler)(action);
            break;
        }
    }
    if(actIdx == actNum)
    {
        printf("did find action %d in status %d\n",action,nowStatus);
    }
    nowStatus = nextStatus;
    printf("next status is %d\n",nowStatus);
}
int main(void)
{
    nowStatus = WAKE;
    execute(EAT);
    return 0;
}

这里简单说明一下execute函数中的执行流程。

  • 判断当前状态合法性

  • 在数组中找到对应状态的行为处理集

  • 在处理集中找到对应的行为

  • 处理结束

这种方式有什么特点呢?可以看到,在需要新加一个动作的时候,只需要在sleepHandler或者weakHandler中添加,完全不影响execute函数的改动。你甚至可以将不同状态分在不同的文件中管理,使得结构更加清晰明朗。

另外一方面,某种状态下,能执行哪些动作,非常清晰

不过这样的写法对于初学者来说不太友好,但是不影响你添加新的内容

有的读者可能会堪虑,在寻找行为的时候,for循环会不会很慢?首先这和case差别不大,通常不会有性能问题,其次除了使用数组,还可以考虑其他数据结构,例如哈希,我在《工作中用不到算法,为什么还要学算法?》中也提到了,数据结构和算法能更好地帮我们解决问题。

总结

本文代码较多,建议在实际环境中运行调试。完整可运行代码也可以阅读原文。

推荐阅读:

工作中几乎用不到算法,为何要学算法?

为何优先选用unique_ptr而不是裸指针?

拥抱智能指针,告别内存泄露

为什么执行自己的程序要在前面加./

关注公众号【编程珠玑】,获取更多Linux/C/C++/算法/计算机基础/工具等原创技术文章。后台免费获取经典电子书和视频资源


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存