面向对象与其说是一个计算机问题,倒不如说他是一个哲学问题。面向对象是用来描述客观世界的一种方法,而不仅仅是一种软件开发思想。
早期的BASIC,ALGOL,C,PASCAL被称为“面向过程”编程语言,这一点从BASIC的“过程”和C语言的“函数”中得到了明显的体 现。面向过程语言模拟的是工作的流水作业方式——定义好每一个流程节点的处理方法,然后把这些方法连接起来,组成一个完整的过程,做到这一步即完成编程。
下面用一个简单的范例来说明面向过程编程的方法(C语言范例):
我们要创建一个菜单项目,在控制台显示两组菜单,并根据菜单的提示完成相应的操作。
- 第一组菜单两项,分别为:输入字符串、退出;
- 第二组菜单四项,分别为:重新输入字符串、显示字符串、翻转字符串、退出。
要完成这一目的,我们把整个过程分为三步:
- 显示菜单
- 响应用户输入
- 根据用户输入进行对应处理
执行完第三步后,流程应该返回第一步,所以这三步组成了一个循环,循环肯定不能为死循环,所以应该有一个退出循环的布尔条件,项目要求要保存一个字 符串,所以还应该有一个保存字符串的缓冲区,代码如下:
0 主流程
1 | #include < stdio . h > |
2 | #include < stdlib . h > |
3 | |
4 | typedef int BOOL; |
5 | |
6 | #define TRUE 1 |
7 | #define FALSE 0 |
8 | |
9 | // 程序的退出条件变量 |
10 | static BOOL g_bQuit = FALSE; |
11 | |
12 | // 保存用户输入字符串的 缓冲变量 |
13 | static char g_szBuffer[ 1024 ] = " " ; |
14 | |
15 | int main( int argc, char * argv[]) |
16 | { |
17 | // 用于接收用户输入菜单项的变量 |
18 | int nInput = - 1 ; |
19 | |
20 | // 循环三个流程 |
21 | while ( ! g_bQuit) { |
22 | ShowMenu(); |
23 | nInput = GetUserInput(); |
24 | DoSomethingWithInput(nInput); |
25 | |
26 | // 在三步流程完毕后,输出一个空行 |
27 | printf( " /n " ); |
28 | } |
29 | system ( " pause " ); |
30 | return 0 ; |
31 | } |
OK,通过上面的代码,我们已经建立起来基本的流程,我们发现这其中缺少了三个重要子流程,ShowMenu,GetUserInput和 DoSomethingWithInput,那么我们进一步完善这三个子流程。
1 ShowMenu流程:在上述代码14行的位置加入如下代码:
1 | /* * |
2 | * 根据情况显示菜单 |
3 | */ |
4 | void ShowMenu() |
5 | { |
6 | // 判断g_szBuffer中是否保存了字符串,根据这一条件,显示第一个菜单或第二个 菜单 |
7 | if (g_szBuffer[ 0 ] = = ' /0 ' ) { |
8 | ShowMenuFirst(); |
9 | } else { |
10 | ShowMenuSecond(); |
11 | } |
12 | printf( " /n请输入您的选项: " ); |
13 | } |
好了,现在我们将ShowMenu流程进一步细化了,根据g_szBuffer变量中是否存储字符串这一条件,分支执行不同的流程:
1.1 ShowMenuFirst流程:在函数 ShowMenu之前 加入如下代码
1 | /* * |
2 | * 显示菜单1 |
3 | */ |
4 | void ShowMenuFirst() |
5 | { |
6 | printf( " 1. 输入字符串 " ); |
7 | printf( " /n0. 退出/n " ); |
8 | } |
1.2 ShowMenuSecond流程:在函数 ShowMenuFirst之后 加入如下代码
1 | /* * |
2 | * 显示菜单2 |
3 | */ |
4 | void ShowMenuSecond() |
5 | { |
6 | printf( " 1. 重新输入字符串 " ); |
7 | printf( " /n2. 显示字符串 " ); |
8 | printf( " /n3. 翻转字符串 " ); |
9 | printf( " /n0. 退出/n " ); |
10 | } |
至此,我们完成了流程一,ShowMenu流程。
接下来,我们进行GetUserInput 流程,这一步很 简单,从用户输入中获取输入的菜单项。
2 GetUserInput 流程:在函数ShowMenu 之 后 加入如下代码
1 | /* * |
2 | * 获取用户输入 |
3 | * 返回:用户输入的结果。 |
4 | */ |
5 | int GetUserInput() |
6 | { |
7 | // 从输入流里读取一个字符 |
8 | int nInput = fgetc(stdin); |
9 | // 刷新输入流,清空输入流中的其它字符 |
10 | fflush(stdin); |
11 | return nInput; |
12 | } |
好了,这就完成了GetUserInput 流程,对于面向 过程初学者来说,很容易犯得一个错误就是:流程冗余,很多同学在该流程中过多的考虑“是不是该判断一下用户输入是否正确”,那么切记,既然这一步流程的作 用就是获取用户输入,那么就不应该考虑任何其它内容。我们继续下一步流程:
3 DoSomethingWithInput流程:在函 数GetUserInput之后 输入如下代码
1 | /* * |
2 | * 根据用户输入执行对应的流程。 |
3 | * 参数:nInput, 用户输入的字符 |
4 | */ |
5 | void DoSomethingWithInput( int nInput) |
6 | { |
7 | switch (nInput) { |
8 | case ' 0 ' : |
9 | AskIfCanQuit(); |
10 | break ; |
11 | case ' 1 ' : |
12 | GetInputString(); |
13 | break ; |
14 | case ' 2 ' : |
15 | if (g_szBuffer[ 0 ] = = ' /0 ' ) { |
16 | ShowError(); |
17 | } |
18 | ShowInputString(); |
19 | break ; |
20 | case ' 3 ' : |
21 | if (g_szBuffer[ 0 ] = = ' /0 ' ) { |
22 | ShowError(); |
23 | } |
24 | ReverseInputString(); |
25 | break ; |
26 | default : |
27 | ShowError(); |
28 | } |
29 | } |
如上所示,我们进一步把DoSomethingWithInput流程细化为了如下几个子流程:
- AskIfCanQuit流程:询问用户是否需要退出;
- GetInputString流程:提示用户输入一个字符串;
- ShowInputString流程:显示用户输入的字符串;
- ReverseInputString 流程:翻转用户输 入的字符串;
- ShowError流程:显示错误信息。
注意case ‘2’和case ‘3’的情况,因为这两部分处于菜单二的选项范围,所以要能够执行这两步,必须确保当前显示的是菜单二,于是这里采用了和ShowMenu内部同样的逻 辑,来判断当前显示的菜单种类。我们继续:
3.1 ShowError流程,在函数DoSomethingWithInput之前 加入如下代 码
1 | /* * |
2 | * 显示错误信息 |
3 | */ |
4 | void ShowError() |
5 | { |
6 | printf( " 对不起,您输入的菜单项不存在,请重新输入。/n " ); |
7 | } |
3.2 AskIfCanQuit 流程,在函数ShowError之后 加入如下代码
1 | /* * |
2 | * 询问用户是否需要退出 |
3 | */ |
4 | void AskIfCanQuit() |
5 | { |
6 | // 接收用户输入字符的变量 |
7 | int nInput = - 1 ; |
8 | |
9 | printf( " /n是否要退出?[Y/N]: " ); |
10 | |
11 | // 从用户输入中读取字符 |
12 | nInput = fgetc(stdin); |
13 | fflush(stdin); |
14 | |
15 | // 判断字符是否为y(忽略大小写),如果是y,则设置全局变量g_bQuit的值,交给 主流程处理退出 |
16 | if (nInput = = ' y ' | | nInput = = ' Y ' ) { |
17 | g_bQuit = TRUE; |
18 | } |
19 | } |
3.3 GetInputString流程,在函数 AskIfCanQuit之后 加入如下代码
1 | /* * |
2 | * 获取用户输入字符串 |
3 | */ |
4 | void GetInputString() |
5 | { |
6 | // 定义接收用户输入的字符 |
7 | int nInput = EOF; |
8 | // 统计用户输入字符个数 |
9 | int i = 0 ; |
10 | |
11 | printf( " /n请输入任意字符串: " ); |
12 | // 循环,直到符合如下条件之一:1、读取到输入流结尾;2、用户输入了一个换行字符 (/n);3、用户输入字符数大于缓冲区长度 |
13 | do { |
14 | // 读取一个字符并存入缓冲区 |
15 | nInput = fgetc(stdin); |
16 | g_szBuffer[i + + ] = ( char )nInput; |
17 | } while (nInput ! = EOF & & nInput ! = ' /n ' & & i < 1024 ); |
18 | |
19 | // 在字符串末尾加上结束符,注意这里要覆盖掉最后一个字符,因为上一个循环导致最后一个 输入的总是无效字符 |
20 | g_szBuffer[i - 1 ] = ' /0 ' ; |
21 | printf( " 输入完毕。/n " ); |
22 | } |
3.4 ShowInputString流程,很简单不多说了,在函数GetInputString之后 加入如下代码
1 | /* * |
2 | * 显示用户输入字符串 |
3 | */ |
4 | void ShowInputString() |
5 | { |
6 | printf( " 您输入的字符串为:%s/n " , g_szBuffer); |
7 | } |
8 |
3.5 ReverseInputString流程,用了一个很简单的翻转算法,在函数ShowInputString 之 后 加入如下代码
1 | /* * |
2 | * 翻转字符串 |
3 | */ |
4 | void ReverseInputString() |
5 | { |
6 | // pH指向字符串起始位置 |
7 | char * pH = g_szBuffer; |
8 | // pE指向字符串最后一个字符(/0字符)的前一个字符(即最后一个有效字符) |
9 | char * pE = g_szBuffer + strlen(g_szBuffer) - 1 ; |
10 | // 定义临时变量 |
11 | char cTemp; |
12 | |
13 | // 循环,直到pH,pE指针指向内存地址重合 |
14 | while (pH < = pE) { |
15 | // 交互字符位置 |
16 | cTemp = * pH; |
17 | * pH = * pE; |
18 | * pE = cTemp; |
19 | |
20 | // pH后移,pE前移 |
21 | + + pH; |
22 | - - pE; |
23 | } |
24 | printf( " 翻转完毕/n " ); |
25 | } |
至此,我们完成了所有的代码。
这段代码很简单,得到需求后,我想大部分同学都能够实现出正确的结果。这里使用这个简单的例子只是想表达一个思想:面向过程的程序设计思想。
从整个代码的构思到书写的过程中,我们看到了一个重要的设计方法:自上而下的程序设计方法,我们一开始并没有过多的去考虑程序细枝末节的细节,而将 注意力全部放在如何完成主要流程上面。我们制定了三大步主要流程,然后将注意力放在流程1(ShowMenu)上,发现它又可以分解为两部流程,于是继续 分解完成ShowMenuFirst流程和ShowMenuSecond流程,而我们所有的代码,都是基于这个原则设计并书写出来的。
这种逐步细化的方法,很容易就将一个看似不容易完成的整体问题,分解为可以轻易完成的若干个小问题,然后各个击破,完成最终的代码设计。
我们其后要开始介绍面向对象,但永远牢记,我们的计算机本质是面向过程的,计算机做事情永远是一步一步的,所以无论我们使用多么先进的程序设计、开 发思想,这一课介绍的方法都将贯穿始终。