多分支实现以及scanf读取错误解决(代码健壮性以及扩展性)

来源:互联网 发布:冰镇西瓜 知乎 编辑:程序博客网 时间:2024/05/01 17:34

 原文地址:http://blog.sina.com.cn/s/articlelist_1586474194_0_1.html 转载请注明出处


要想成为一名优秀的软件工程师,你必须对自己的代码精益求精,哪怕是最简单的问题也需要仔细考虑,在尽量减少Bug出现的可能性的同时还需要提高程序的扩展性。下面就举一个大家认为很简单,也非常熟悉的一个控制台菜单选项程序。

1.1第一个版本V0.1使用if else做分支。这个可能是初学者最先想到的方式。(scanf问题)

程序清单   用if else作分支处理(V0.1)

void CmdRunning()

{

    int iCmdNum;

    

   

                  printf("请选择:0. 退出;1. 新建文件;2. 打开文件;3.保存文件\n");

                  scanf("%d",& iCmdNum);   //jlu 修改:可以在此代码之下使用fflush(stdin); 来清除输入缓存

                  if (0 == iCmdNum) {

                         printf("谢谢使用,再见!\n");

                  }

                  else if (1 == iCmdNum) {

                         CreateFile();

                  }

                  else if (2 == iCmdNum) {

                         OpenFile();

                  }

                  else if (3 == iCmdNum) {

                         SaveFile();

                 }

                 else {

                         printf("对不起,你选择的数字不存在,请重新选择\n");

                 }

        }while(iCmdNum!=0);

}

    细心的同学可能会发现,这段程序存在一个很严重的Bug,比如,运行程序后输入字符’a’(非数字),回车,死循环出现了。为什么会出现这种情况呢?原因是scanf只能将缓冲区中的数字(使用了%d)读入iCmdNum,并清空缓冲区,而我们输入的是非数字,那么scanf读入失败,失败了它也就不会去清空缓冲区,这样造成的结果就是,我们不需要再输入其它字符,scanf每次都会去读缓冲区,每次都失败,每次都不会清空缓冲区,当下次再来读时发现缓冲区中有数据就不会停止等用户输入,接着又进入下一次的循环,死循环就形成了。


1.2 原文作者解决scanf问题

V0.1版本中,我们提到了输入参数的合法性检查问题,不过还是先介绍另一个非常重要的理念——“健壮性”。在大多数本科教材中也只是提到了这个概念,却并没有深入的去探讨,其实不然,对于一个合格的程序员来说,软件的健壮性理念必须放在首位。很多到企业实习的学生编写的代码,一般都能够正确地实现程序的功能,一些学生甚至还会做到功能上的扩展,但是所有的学生都有一个共同的毛病,从来不做异常处理,只要用户输入不合法,则程序很容易崩溃,这是不允许发生的,也是用户绝对不能容忍的。虽然软件在发布前必须经过大量的测试,但希望初学者必须养成良好的编程习惯。

    下面开始讨论V0.1版本软件接受用户输入的合法性判断。为什么要重点判断这个问题呢?道理很简单:“用户都是傻瓜”,当然,我们不是说使用者智商有任何问题,而是开发人员应该这么去想。开发者无法预知用户会输入那些奇奇怪怪的内容,但是好的程序都必须能够处理那些看来莫名其妙的问题。我们到底将如何解决这样的问题呢?

1.方法1

    “scanf("%d",&iCmdNum);”语句是V0.1版本出现死循环的关键,那么我们不妨先从这里入手。其实只要将“%d“改为“%c”,即可解决死循环问题。比如:

    scanf("%c", &chCmd);                                        // chCmd是一个字符变量

    iCmdNum = atoi(&chCmd);                                                                                      // chCmd转换为整型数值

    为什么这样做就能够解决死循环问题呢?我们不妨编写不同的测试用例(在这里仅仅需要输入不同的数值即可)试试。如果出现问题,将如何解释?

       ☛ 提示

l         控制台中的所有输入都被认为是字符;

l         回车键是一个或两个字符(不同的操作系统和编译器有不同的解释,而在Windows控制台中回车键的ASCII码为10);

l         查看atoi的使用方法,注意它执行失败后的返回值。

    为什么将“%d“改为“%c”即可避免死循环?大家知道V0.1版本出现死循环的原因完全是因为scanf从缓冲区读取值失败所引起的。因为在输入输出流中所有的类型都会被当作字符来看待,那么当使用%c来读入chCmd时,不管输入什么样的值都会成功。当scanf成功之后,则会将缓冲区中的字符清空 (实际上是将“流指针”后移),等待下一次用户的输入,所以它不会出现死循环。

2.方法2

       既然问题是由用户输入函数带来的,那么我们即可使用其它标准库函数来解决这个问题。根据控制台菜单程序用户输入的特点,我们不妨输入单字符来触发功能,即通过getchar函数来实现用户交互。但是,我们一定要注意其中的细节,该函数的原型

    int getchar(void)

    在常见的c编译器里面,char类型取值范围为[-128, 127],类似EOF等宏的值则在此取值范围之外,那么EOF将不会被正确地读入,那么

       char c= getchar();

则是一个隐藏很深的bug。在这里我们将接受输入值的变量定义为int

                      程序清单2   if else做分支处理并用字符作比较(V0.2)

    void CmdRunning()

    {

        int iCmdNum=0;

        

        do {

             printf("请选择:0. 退出;1. 新建文件;2. 打开文件;3. 保存文件\n");

             iCmdNum = getchar();

             fflush(stdin);                                              // 清空缓冲区

             if ('0' == iCmdNum) {

                  printf("谢谢使用,再见!\n");

             }

             else if ('1' == iCmdNum) {

                   CreateFile();

             }

             else if ('2' == iCmdNum) {

                   OpenFile();

             }

             else if ('3' == iCmdNum) {

                   SaveFile();

             }

             else {

                   printf("对不起,你选择的数字不存在,请重新选择!\n");

             }

        }while('0' != iCmdNum);

     }

 1.3  利用switch语句实现控制台程序 

    与此同时,由于使用if else处理多分支选项显得更加繁琐且效率低下,它必须一个一个分支的判断,而处理多分支比较容易想到的就是switch 语句了。为了避免出现V0.1中的死循环,我们后面的例子将使用另外一个输入函数,代码详见程序清单3

                          程序清单  switch语句做分支处理V0.3 

      void CmdRunning()      

2      {

              int iCmdNum = 0;1

4

        do{

                       printf("请选择:0. 退出;1. 新建文件;2. 打开文件;3. 保存文件\n");

                       iCmdNum = getchar();                                      // 获得一个字符

                       fflush(stdin);                                                                                                       // 清空缓冲区

                       switch(iCmdNum) {

10                             case '0':

11                                            printf("谢谢使用,再见!\n");

12                                            break;

13                            case '1':

14                                           CreateFile();

15                                           break;

16                            case '2':

17                                           OpenFile();

18                                           break;

19                            case '3':

20                                           SaveFile();

21                                           break;

22                            default:

23                                           printf("对不起,你选择的数字不存在,请重新选择!\n");

24                         }

25               }while('0' != iCmdNum);

26      } 

 

    对于初学者来说,如果能够独立地写出这个版本,则说明已经达到了本科毕业的基本要求。现行教科书上的许多例子,几乎都是以这种形式出现的,而采用这种思路设计的代码,其扩展性却很差。


1.4 利用函数指针实现多分支选项


在程序设计时,我们经常用ifswitch语句作为条件判断,调用不同的函数,比如,在菜单驱动软件中,程序提示用户从菜单中选择一个选项,每一个选项都有一个不同的函数来完成其相应的功能。

    如果使用函数指针,将指向每一个函数的指针存储在一个指向函数的指针变量中。例如,函数“void CreateFile();”的CreateFile会被解释为该函数的指针,我们可以将其赋给同类型的指针,从而实现一个函数指针调用不同的函数,这样会使程序更简洁、更专业。下面将利用函数指针来调用switch中的各个函数,详见程序清单4

                        程序清单  利用函数指针实现多分支选项(V0.4)

1    void CmdRunning()

2   {

3       int iCmdNum = 0;               

      void (*pCmd)() = NULL;

         

      do{

7             pCmd = NULL;

8             printf("请选择:0. 退出;1. 新建文件;2. 打开文件;3. 保存文件\n");

9             iCmdNum = getchar();

10            fflush(stdin);

11            switch(iCmdNum){

12              case '0':

13                        printf("谢谢使用,再见!\n");

14                        break;

15              case '1':

16                       pCmd = CreateFile;

17                       break;

18              case '2':

19                       pCmd = OpenFile;

20                       break;

21              case '3':

22                       pCmd = SaveFile;

23                       break;

24              default:

25                       printf("对不起,你选择的数字不存在,请重新选择!\n");

26              }

27              if (NULL != pCmd){

28                 pCmd();                                          // 与“(*pCmd)();等价

29              }

30        }while('0' != iCmdNum);

31    }

这种方式代码虽然简洁,但是这种形式的扩展性很差。如果想要扩展功能,需要修改两个地方,第一、打印的帮助信息,它不能做到动态绑定,这也是程序员在开发过程中很容易被忽略的地方,因为它不会影响程序的使用功能;其次,对于添加case语句及处理函数,还有没有更好的实现方式呢?后面的例子会给大家更深入的讲解这个问题。

由此可见,除了可以通过“函数名”调用函数之外,还可以通过“指向函数的指针变量”调用相应的函数。但用函数名调用函数,只能调用所指定的一个函数;而如果通过指针变量调用函数,则更加灵活,可以根据不同情况先后调用不同的函数。


0 0
原创粉丝点击