原文地址:http://blog.sina.com.cn/s/articlelist_1586474194_0_1.html 转载请注明出处
要想成为一名优秀的软件工程师,你必须对自己的代码精益求精,哪怕是最简单的问题也需要仔细考虑,在尽量减少Bug出现的可能性的同时还需要提高程序的扩展性。下面就举一个大家认为很简单,也非常熟悉的一个控制台菜单选项程序。
1.1第一个版本V0.1使用if else做分支。这个可能是初学者最先想到的方式。(scanf问题)
程序清单 1 用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。
程序清单 3 用switch语句做分支处理(V0.3)
1 void CmdRunning()
2 {
3 int iCmdNum = 0;1
4
5 do{
6 printf("请选择:0. 退出;1. 新建文件;2. 打开文件;3. 保存文件\n");
7 iCmdNum = getchar(); // 获得一个字符
8 fflush(stdin); // 清空缓冲区
9 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 利用函数指针实现多分支选项
在程序设计时,我们经常用if或switch语句作为条件判断,调用不同的函数,比如,在菜单驱动软件中,程序提示用户从菜单中选择一个选项,每一个选项都有一个不同的函数来完成其相应的功能。
如果使用函数指针,将指向每一个函数的指针存储在一个指向函数的指针变量中。例如,函数“void CreateFile();”的CreateFile会被解释为该函数的指针,我们可以将其赋给同类型的指针,从而实现一个函数指针调用不同的函数,这样会使程序更简洁、更专业。下面将利用函数指针来调用switch中的各个函数,详见程序清单4。
程序清单 4 利用函数指针实现多分支选项(V0.4)
1 void CmdRunning()
2 {
3 int iCmdNum = 0;
4 void (*pCmd)() = NULL;
5
6 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