第5章分而治之———模块化程序设计 学习目标: .理解模块化程序设计的基本思想。 .理解函数的定义、函数原型的概念。 .掌握函数调用的方法。 电子教案.理解函数调用的过程和函数参数传递机制。 .学会使用标准库中的函数。 迄今为止,我们已经学习了三种控制程序的结构:顺序结构、选择结构和循环结构。如 果能灵活运用这三种控制结构,使用堆叠嵌套技术,毫不夸张地说,你已经可以解决绝大多 数问题了。前几章解决的问题都相对比较简单,问题的求解算法都比较明显,写出的程序一 般是十几行,最多不过几十行,大多还不到一页纸的长度。但是很多问题往往求解算法比较 复杂,常常要采用自顶向下、逐步求精的方法确定出算法,而且有很多问题的实现代码可能 多达几百行,几千行,甚至几万行。这种规模的代码还能像前几章那样都写在一个main函 数里吗? 回答应该是可以! 但是,设想一下成千上万行代码都写在一个main里,该有多么 难以控制啊! 一个规模比较大的问题,往往都要分解成若干个相对比较小的子问题,每个子问题还可 以再分成若干个更小的子问题,这个过程是自顶向下、逐步求精的过程,而且是逐层进行的。 自顶向下、逐步求精的过程为模块化实现提供了具体的方法。每个子问题都可以对应一个 独立的功能模块(函数)。要求解的问题经过模块化之后,在主函数main里只包含顶层分 解的模块函数调用,每个被调用的模块都独立于主函数之外,作为主函数的工具。 三种程序控制结构与模块化相结合才是真正的结构化程序设计。 本章详细讨论如何建立函数模块,如何使用函数模块,函数模块之间是如何联系在一起 的,如何有效地管理众多的函数模块———文件模块,如何建立自己的函数库,定义用户使用 的接口,比较系统地研究一下模块化程序设计的基本方法 。 本章要解决的问题 : .再次讨论猜数游戏模拟问题; .是非判断问题; .递归问题; .简单的计算机绘图问题; .学生成绩管理系统初步。 5.再次讨论猜数游戏模拟问题 1 视频问题描述 : 问题同4.7节的问题描述,这里略。输入输出样例也请参考4. 7节。 153 问题分析: 在4.7节已经采用自顶向下、逐步求精的方法研究了这个问题。已经认识到要解决一 个比较复杂的问题,不可能一开始就确定一个十分精细的解决方案,一般要经历一个从抽象 到具体,逐步明晰的过程。开始先勾画出求解方案的一个比较粗糙的轮廓,确定一个比较抽 象的概念,然后再逐步细化,把抽象的东西逐渐细化到可以实现的具体步骤。自顶向下、逐 步求精的过程是一层层分解的过程。如果设猜数游戏模拟的顶层是问题的原始抽象描述, 经过第一次分解问题变成了两个子问题。 主模块算法: ① 让计算机“想”一个数; ② “猜数”过程模拟; ③ 问是否继续玩,回答y/Y,返回到①,否则程序结束。 其中的①和②的每一步还都比较抽象,但问题已经被模块化,即整个问题的求解分成两个子 模块①和②,分别命名为makeMagic和guessNumber。makeMagic模块的功能是计算机 “想”一个数,guessNumber模块的功能就是模拟一次猜数游戏的全过程,直到猜中为止。 至于makeMagic怎么想,guessNumber怎么猜现在先不用考虑,可以假设它们都已经解决 了。这样,main函数就变得非常简单,就是顺序的调用makeMagic模块和guessNumber模 块。这个比较粗糙/抽象的主模块算法,如果用流程图表示则称为主流程,如图5.1所示。 如果把main函数认为是一层,两个子问题对应的子模块认为是第二层,这样模块之间形成 了一个层次结构,如图5.2所示。这个层次结构表明了主函数和子模块之间的层次调用关 系,图中单向的箭头线表示调用,意识是猜数游戏main 函数调用makeMagic 和 guessNumber模块。 图5.1 猜数游戏的主流程图5.2 猜数游戏程序的层次结构 主函数模块的实现见程序清单5.1。 程序清单5.1 #001 guessnumberNew.c #002 int main(void){ #003 char a; 源码5.1 154 #004 int magic; #005 srand(time(NULL)); //设置随机数的种子 #006 printf("Welcome to GuessNumber Game\n"); #007 do{ #008 magic=makeMagic(); #009 printf("I have a magic number between 1 to 1000, please guess:"); #010 guessNumber(magic); #011 printf("Continue or no? Y/N\n"); #012 a=getch(); //输入一个字符赋给a #013 }while( a=='Y' || a=='y'); #014 return 0; #015 } 其中,两个关键步骤#008行和#010行分别调用了函数makeMagic()和guessNumber (magic),在这里不管它们是如何实现的,只关心它们的功能,它们的实现代码是在main函 数之外的其他某个地方。 主流程和层次结构清楚之后,接下来就可以进一步研究每个子问题(即每个模块)的解 决方法了。如果子问题还很复杂、抽象,那就还要把每个子问题再次进行分解,每个分解出 来的子子问题又对应一个更小的模块,它们合起来构成本模块的解决方案。如果经过第一 次划分之后的子问题已经很容易求解了,就直接设计这个子问题的详细算法即可。对于猜 数游戏模拟问题,第一次分解求精得到的模块“让计算机想一个数”已经比较简单了,可以直 接给出它的算法如下。 makeMagic模块算法 1.1 使用随机函数rand()产生一个1~1000的数number 对于猜数过程模拟模块,第二步判断是否猜中,又加细求精了一次,也可以做成一个第 三层的模块,由于它过于简单,所以就把它细化在猜数模块中展开了,即 guessNumber模块算法 1.2.1 接收用户猜的数guess 1.2.2.1 如果guess>number,提示toohigh返回到1.2.1 1.2.2.2 如果guess<number,提示toolow 返回到1.2.1 1.2.2.3 如果猜中,转到1.2.3 1.2.3 输出祝贺信息 在C/C++中,每个功能模块的实现程序用自定义的“函数”表示,其形式与main类似, 但是各自都有自己的名字。这两个函数的定义如下: makeMagic函数定义: #001 /* #002 * 函数功能: 产生一个1~1000 的随机数,并反馈已经产生的信息 #003 * 入口参数: 无 #004 * 返回值: 计算机"想"好的随机整数 #005 */ #006 int makeMagic(void){ #007 int magicNumber; 源码 155 #008 magicNumber=rand()%1000 +1; //产生随机数 #009 return magicNumber; #010 } guessNumber函数定义: #001 /* #002 * 函数功能: 猜数过程模拟 #003 * 入口参数: 整型magic,计算机想好的数 #004 * 返回值: 无 #005 */ #006 void guessNumber(int magic){ #007 int guess; #008 do{ #009 scanf("%d",&guess); #010 if(guess >magic) #011 printf("Wrong, too high! try again!\n"); #012 else if(guess <magic) #013 printf("Wrong, too low! try again!\n"); #014 }while(guess <magic || guess >magic); #015 printf("Congratulation! You are right!\n"); #016 } 关于自定义函数的具体细节在本节进行详细讨论。 5.1.1 模块化思想 现在我们总结一下刚才的讨论。在自顶向下、逐步求精的过程中,最初是比较粗糙的算 法,把比较抽象的猜数游戏模拟归结为两个子问题,每个子问题对应一个模块。如果每个子 问题能够很容易地得到解决,整个问题便得到了解决,如果子问题还比较抽象,就继续拆分 成更小的子问题,又产生很多更小的模块,以此类推,直到很具体地能够解决为止。这个从 抽象到具体的过程蕴藏着一种层次结构、模块结构,它所体现的就是模块化程序设计的基本 思想,它是解决复杂问题或大规模问题的一种行之有效的策略———“分而治之”策略。 模块化程序设计使得一个比较大的问题转化为若干个相对独立的、规模较小的子问题, 这给软件开发带来了很多方便和好处。 . 整个系统的开发可以由一个团队合作完成,每个成员只需完成其中的一部分; . 开发一个模块不必知道其他模块的内部结构和编程细节,只需知道它所需要的那些 模块的接口,每个模块可以独立开发; . 模块之间通过特别的消息传递机制(函数调用,参数传递)有机地结合在一起; . 模块化系统具有层次结构,降低了复杂性,因此具有易读性,容易阅读和理解; . 模块化系统具有可修改性,对系统的修改只涉及少数部分模块; . 模块化系统具有易验证性,每个功能模块可以独立测试验证,而且由于功能单一、规 模较小,所以容易验证; . 模块具有可重用性,每个功能模块可以反复使用,因此也称可复用性。 156 注意:模块化程序设计要求每个功能模块的规模不要过大,模块的功能应该单一,即遵 循模块功能的高内聚基本原则。模块的接口(名字和参数)应该尽可能简明,不同模块之间 应尽可能少关联,即遵循低耦合的基本原则。 5.1.2 函数定义 C/C++结构化程序设计把每个功能模块用函数表示,这个函数从功能上来看有点像数 学函数,但表现形式和实现方法都与数学截然不同。从第一个Hello程序开始,我们就已经 接触到C/C++的函数了,一个是main函数,它是每个软件或程序必须有的;另一个是用来 输出信息的printf函数,后来又陆续接触了scanf、sqrt等。不管是哪个函数,它们定义的结 构都是一样的,都像我们已经看到的main函数的样子。函数定义(简称函数)的一般形式 如下: /* * 函数注释 */ 返回值类型函数名( 参数列表) //函数头 { 声明语句部分; //语句注释 执行语句部分; //语句注释 [return(表达式);] } 从整体上来看,函数定义是一段有注释、有名字的程序代码,它由三部分组成:函数注 释、函数头和函数体(大括号括起来的部分,声明语句+可执行语句+return)。下面分别详 细讨论。 1.函数头 一个函数定义的头也由三部分构成: (1)返回值类型,这个类型用来说明函数使用之后能够返回什么类型的值,因此也有人 说它是函数类型。编译器的各种内置数据类型,如整型(int),浮点型(float、double),字符型 (char),逻辑型(bool)等均可以作为函数类型,甚至第8章要学习的自定义的各种类型都可 以作为函数类型。如果函数使用之后没有要返回的值,就要用void代替。 (2)函数名,它是函数的标识符,其命名规则同变量名的命名规则类似,在C语言中两 个不同的函数不能同名(C++允许,称为函数重载)。 (3)参数列表,它是逗号隔开的一些列表项,每一项说明一个参数。它声明了一组参数, 参数之间用逗号隔开。这组参数是将来函数被调用时函数能够接收的参数。声明格式为 参数类型参数名称,参数类型参数名称,… 注意,这个参数列表必须放在一对小括号之内。一对小括号是函数的重要特征,只有看 到小括号,才能确定其前面的名字是函数名。例如: int max2(int a, int b); 函数的参数列表是“inta,intb”,说明当使用函数max2时可以接受两个整型参数。 157 每个参数的声明格式“参数类型参数名称”很像一般变量的声明格式。但它只是在形 式上给出了参数的类型说明,因此它叫形参。在未使用那个函数的时候,形参并不是什么变 量,只有这个函数被使用的时候形参才作为那种类型的变量自动产生,才有实际值。既然形 参是形式上的参数,因此用什么名称应该无关紧要,但是参数的顺序必须明确,如函数void printRectangle(inth,intw)的功能是打印一个h行w列的字符图案,第一个参数的意义是 行数,而第二个参数的意义是列数,两个参数如果交换一下顺序,意义就不同了。 参数列表也可以为空,但一般要用void表示,如果没有用void,而是空白,C语言编译 器认为任何参数都可以接受。如大家熟悉的main,如果没有参数,标准的写法都应该写成 intmain(void),尽管写成intmain()也没有出错,但还是存在潜在的危险。7.6.1节将看到 main函数也允许有参数。 总之,一个函数定义的参数列表必须明确它有几个参数,每个参数的类型是什么,各个 参数的顺序如何。 2.函数体 一对大括号括起来的所有语句是函数定义的主体,简称函数体。函数体是一些语句的 集合,就像我们在main函数中看到的一样,各种语句都可以出现在函数体中,包括变量声 明语句和各种可执行语句,如赋值语句、输入/输出语句、函数调用语句、判断选择语句、循环 语句等。要实现一个由函数头确定的函数的功能必须有正确的函数体,也就是必须熟练掌 握前面已经学过的变量如何声明,变量的输入输出语句,三种程序控制结构的语句(常常包 含复合语句)。 当函数的返回值类型为非void时,一般至少要包含一个返回语句: return(表达式); 它把表达式的值返回给将来使用它的语句———函数调用语句。其中,表达式的值的类 型与函数的返回类型一致。return中的表达式可以有各种各样的形式,只要它有适合于返 回类型的值即可。例如,“return1;”或“returna;”或“return(a+b);”均可。return表达 式两边的括号也可以省略。当返回值类型是void的时候,可以省略return语句,这时右大 括号“}”起到返回的作用。 return语句常常位于函数体的末尾,但有时也在函数体的内部,一般都是当某个条件为 真时提前结束函数的执行。 函数头和函数体一起构成了编译器识别的函数定义。从函数的定义可以看出,一个函 数实际上就是一组语句被封装在了一起,用一个名字来表示,这组语句在被执行之前会通过 参数带进一些信息,在被执行之后将完成一个特定的任务,其结果通过return语句或其他 形式返回。因此,可以说一个函数就是具有某种特别功能的一种工具。一般来说,使用函数 图5.3 函数是一个黑盒子 的人比较关心的是要提供给函数什么样的参数它才能执行,函 数执行后其结果会是什么,并不太关心函数内部到底是怎么工 作的、如何实现的。因此,人们常常把函数看成是一个黑盒子, 如图5.3所示。 3.函数的注释 大家都知道,注释部分是被编译器忽略的,它仅仅为了方便阅读而存在,但它是比较重 158 要的一部分。作为一个程序员或者说函数的设计者,不仅要能够设计出符合上述C/C++语 言格式的、好用的函数,还要考虑代码的可读性。因此,一方面,必须熟悉前面几章学过的基 本程序结构,按照大家公认的程序结构风格写出正确的函数体代码(含各种语句的注释);另 一方面,一个好的函数定义,还应该包含一些必要的注释。常用的注释风格如 /* * 函数功能: * 入口参数: * 返回值: */ 又如 /* * 函数名称: * 使用方法: 给出具体的调用形式 * ------------------------------- * 函数功能描述(参数说明,返回值说明) */ 下面看几个函数定义的例子。 【例5.1】 定义一个函数,求两个整数的最大值。 定义一个函数要从函数头的三要素出发,即要确定函数名称,函数的参数列表及函数的 返回类型。此函数的功能是对任意给定的两个整数,求它的最大值。这里两个整数是变化 的,因此必须让函数具有两个整型参数。函数的结果是一个整数,所以返回类型应该是整 型。函数的命名应该尽量有意义,应该看到名字就能基本知道函数的功能,因此给这个函数 命名为max2int,意思是2个整数的最大值。完整的函数定义如下: #001 /* #002 * 函数功能: 求两个整数的最大值 #003 * 函数参数: 两个整数 #004 * 返回值类型: 整数 #005 */ #006 int max2int(int a, int b){ #007 return (a>b? a:b); //返回a 和b 中的较大者 #008 } 这个函数的函数体非常简洁,只有一个语句,但功能已经实现。它直接返回了条件运算表达 式的值,结果是两个参数值的较大者。这个函数的具体使用方法见5.1.3节。 【例5.2】 定义一个函数,打印h行w列的矩形图案。 这个函数要打印一个h行w 列的图案,显然函数的参数列表应该是两个整型参数,第 一个参数表示行数或者是高度,第二个参数表示列数,也可认为是宽度。它的功能是打印图 案,没有计算的结果要返回,因此返回值类型应该是void。函数取名为printRectangle,完整 的函数定义如下: #001 /* 159 #002 * 函数功能: 打印矩形 #003 * 函数参数: 两个整数 #004 * 返回值类型: 无 #005 */ #006 void printRectangle(int h, int w){ #007 int i,j; #008 for(i=0;i<h;i++){ #009 for(j=0;j<w;j++) #010 printf("*"); //循环打印*号 #011 printf("\n"); #012 } #013 } 【例5.3】 定义一个函数,显示一个菜单界面。 当一个问题比较复杂时,经过分解会有很多子问题或者具有很多功能模块,这时可以给 用户一个菜单界面提示,供用户选择,用户选择不同的功能就去执行不同的功能模块。而且 这个界面是始终要显示在用户面前的,需要多次调用才能达到这样的效果。因此,有必要定 义一个显示菜单界面的函数,函数命名为menu,函数定义的完整实现如下: #001 /* #002 * 函数功能: 显示系统主界面 #003 * 入口参数: 无 #004 * 返回值: 无 #005 */ #006 void menu(void){ #007 printf(" |-------------------------------------------|\n"); #008 printf(" | 请输入选择的数字字符 |\n"); #009 printf(" |-------------------------------------------|\n"); #010 printf(" | 1--添加信息(append record) |\n"); #011 printf(" | 2--显示信息(list record) |\n"); #012 printf(" | 3--删除信息(delete record) |\n"); #013 printf(" | 4--修改信息(modify record) |\n"); #014 printf(" | 5--查询信息(search record) |\n"); #015 printf(" | 0--退出(exit) |\n"); #016 printf(" |-------------------------------------------|\n"); #017 printf(" \n"); #018 } 它是一个既无入口参数又无返回值的函数,调用它就会在屏幕上显示学生成绩管理系统的 主界面菜单。 【例5.4】 定义猜数游戏模拟的makeMagic函数。 它的功能是产生一个随机“想”出来的数,不需要参数,返回类型是一个整型,见本节开 始的函数定义。 【例5.5】 定义猜数游戏模拟的guessNumber函数。 它的功能是模拟猜数过程,需要知道计算机想的数是什么,因此要有一个整型参数,没 160 有无返回值。见本节开始的函数定义。 从前面的讨论可以知道,任何函数都是自己独立的。即函数的定义是不能交叉,也不可 以嵌套的。下面两个函数的定义func1和func2彼此嵌套在一起是不允许的。 #001 void func1(void){ #002 … #003 void func2(void){ #004 … #005 } #006 … #007 } 注意:一个函数的规模一般不要过大,一般控制在50行以内为宜。一个函数的功能也 应尽可能单一,不要让一个函数的负担过重。 5.1.3 函数调用 函数是自顶向下、逐步求精的求解过程中分而治之的结果,每个子问题均可以定义为一 个函数,函数模块之间呈现一种层次结构,最顶层的是主函数main。一般来说,下一层的函 数模块是为上一层的函数模块服务的,也就是说,上一层的函数模块要使用下一层的函数, 当然如果需要的话,下一层的函数模块也可以使用上一层的函数模块,同层的函数模块也可 以互相使用。除了main函数具有特殊的地位之外,其他所有函数都是彼此独立的,均可以 彼此使用。在一个函数中使用另一个函数称为函数调用(Call),使用者称为主调函数(或调 用函数),被使用者称为被调用函数。函数调用的一般形式是 函数名(实参列表) 其中,函数名后面的一对小括号是必需的,实参列表是逗号隔开的,它与函数定义中的形参 列表一一对应,常量、变量或表达式均可以作为实参,只要类型匹配,可以提供形参需要的 “值”即可。函数调用可以独立使用,形成一个函数调用语句,如调用printRectangle函数打 印一个5行10列的矩形图案: printRectangle(5,10); 也可以作为其他语句或表达式的一部分,如函数makeMagic调用的结果赋给变量magic: magic=makeMagic(); 注意,当我们要使用某个函数时,必须从以下三个方面着手。 首先要知道那个函数的名字是什么,函数的功能是什么。 然后看看它有无参数;如果有参数,还要进一步确认: ① 有几个参数; ② 参数都是什么类型的; ③ 参数的先后顺序如何。 最后要知道函数是否有返回值,返回值是什么类型,返回值的意义如何。 【例5.6】 使用函数max2int和函数printRectangle。 161 以max2int函数和printRectangle函数为例,看看它们是如何被调用的。这两个函数 都是有参数的。函数max2int有一个整型返回值,而函数printRectangle无返回值。 C/C++语言规定,一个函数在被调用之前必须定义或声明,否则编译的时候就会出现 警告和错误,例如程序中使用函数func: warning: implicit declaration of function 'func' undefined reference to 'func' 这与一个变量在使用它之前必须先用声明定义一样。编译器要知道被调用的函数是什么样 的才允许使用。怎么让编译器知道呢? 方法之一就是把函数定义的代码放在函数调用语句 代码之前,这个“之前”未必是相邻,只要在前面即可。如果要在main函数中使用max2int 和printRectangle函数,把max2int和printRectangle两个函数的定义放在main函数之前 即可,见程序清单5.2。 程序清单5.2 #001 /* #002 * useFuncs.c: 函数使用举例,函数定义在函数调用之前 #003 */ #004 #include<stdio.h> #005 #include<stdlib.h> #006 /* #007 * 函数功能: 求两个整数的最大值 #008 * 函数参数: 两个整数 #009 * 返回值类型: 整数 #010 */ #011 int max2int(int a, int b){ #012 return (a>b? a:b); //返回a 和b 中的较大者 #013 } #014 /* #015 * 函数功能: 打印矩形 #016 * 函数参数: 两个整数 #017 * 返回值类型: 无 #018 */ #019 void printRectangle(int h, int w){ #020 int i,j; #021 for(i=0;i<h;i++){ #022 for(j=0;j<w;j++) #023 printf("*"); //循环打印*号 #024 printf("\n"); #025 } #026 } #027 /* #028 * 主函数: 使用max2 和printRectangle 函数 #029 */ #030 int main(void){ 源码5.2