实验五 模块化的程序设计
实验目的
(1)理解自顶向下,逐步细化的模块化设计思想划分子模块;知道模块化程序设计方法。 (2)熟悉函数的定义、函数的声明和函数的调用,理解函数调用的实现过程。
(3)理解函数调用时的数据传送机制,通过参数将主调函数的数据传递到被调函数,运用
return语句将被调函数的处理结果返回主调函数。
(4)通过设置断点和单步执行,观察子函数的执行路径,和子函数中变量的值的变化。
实验范例 引导任务
任务1会变化的三角形 任务2 计算生日相差几天
任务1 会变化的三角形
任务描述
在屏幕上打印一个三角形,如图5-1所示,运行程序时输入三角形的级别n,可以打印不同大小的三角形。
图5-1 输出可变的三角形
任务解决
任务解决路径:
图5-2 任务1 解决路径
第一步:学习无参无返回值函数的定义、声明和调用;
第二步:了解函数参数的作用,学习使用带参的函数构建多层模块程序的构建; 第三步:学习带参无返回值函数的设计;
1.编写打印三角形的函数
(1)问题分析
要在屏幕上打印一个三角形,可以直接在main函数中使用printf语句完成,如下程序代码所示:
#include int main() {
printf("打印一个三角形\n ");
printf(" *\n");
printf(" ***\n");
printf("*****\n");
printf("打印结束\n");
return 0; }
在本任务中,要求调整程序的结构,写一个打印三角形的无参无返回值函数,然后在main函数中调用它实现打印三角形。程序员编写的函数称为用户自定义函数,相对于系统函数而言,不是系统已写好的代码,需要程序员自己定义函数和编写实现函数的程序代码。无参无返回值函数通常执行一个具体的操作,无计算结果。 (2)函数的应用
使用用户自定义函数分三步:函数定义,函数声明和函数调用。 ①无参无返回值函数的定义
无参无返回值函数定义的一般形式为:
void函数名(void) { ...... }
一个打印三角形的函数的定义可以为:
void triangle(void) { printf(" *\n"); printf(" ***\n"); printf("*****\n"); }
triangle是函数名,函数名应能体现函数的功能,以增强程序的可读性,这个函数的功能是打印一个固定的三角形。函数类型为void,表示没有返回值,形参表中写void,表示没有参数,void可以省略,但一对圆括号不能省略,表示函数运算符。 ② 函数的声明
用户自定义函数要先声明后调用。无参无返回值的函数声明的一般形式:
void 函数名(void);
例如triangle函数的声明语句为: void triangle(void);
函数声明所需要的信息都在函数的首部,书写函数声明时可以复制函数首部,加分号。 ③ 函数调用
无返回值函数的调用形式为: 函数名();
例如triangle函数的调用语句为:
triangle();
虽然triangle函数没有参数,实参列表为空,但一对圆括号不能省略,而且圆括号中不能写void。这种从函数名开始的调用形式,称为函数语句。
(3)编写程序
最后关注这三个组成部分在程序中出现的位置,程序sample05_01.cpp演示了一个规范的程序代码格式。
【例5.1 sample05_01.cpp】 /* 打印一个三角形*/ #include //函数声明
void triangle(void);
int main() {
printf("打印一个三角形\n"); //函数调用
triangle();
printf("打印结束\n"); ; return 0;} //函数定义
void triangle(void) {
printf(" *\n"); printf(" ***\n"); printf("*****\n"); }
在程序中,函数定义的位置最好在main函数后面,因为程序从main函数开始执行,
到main函数的最后一句语句结束,我们阅读程序也习惯从main函数开始。函数和函数之间没有先后关系,也没有包含关系,不能在一个函数的内部定义另一个函数,每个函数都是独立定义的。
函数声明通常放在预编译命令的后面,main函数之前。这是一个全局位置,函数声明对所有的函数有效。如果函数声明放在另一个函数的内部,那只对该函数有效。
函数调用按程序的需要进行,一个函数定义并声明后,可以多次调用。程序的执行从main函数的第一条可执行语句开始,执行完printf语句,输出字符串“打印一个三角形”,顺次执行函数调用语句“triangle();”,程序转向triangle的函数定义处,执行triangle函数中的语句,执行完后回到main函数,接着向下执行printf语句和return语句,main执行结束,程序结束。
思考:如果要在垂直方向上连续输出三个三角形,如图所示,该如何修改你的程序呢?
图5-3 输出三个三角形
提示:函数是通用项目,可以多次调用。
2. 使用通用函数编写三角形函数
先阅读两个通用功能函数line和point,它们实现基本的显示线段功能。 line函数定义如下:
void line(intstart,int end ) line函数的作用是画一条从start开始{ inti; 到end结束的由*组成的线段,start和end for(i=1;i
printf(" ");
printf("*\n") ; //结束星号和回车 }
line函数和point函数与前面的triangle函数相比,增加了两个参数变量start和end,这两个变量在函数调用时可以接受两个整数,决定画出的线段的长短。参数变量定义的方法与一般函数体内定义变量的方法相似,要设定变量的数据类型,但不同的是每个参数变量必须单独定义。带参无返回值的函数定义一般格式如下:
void 函数名( 形参变量1, [ 形参变量2,......]) {
....... }
line函数的两个参数变量都是整型,也不能共用一个int类型,下面的定义形式是错误的,end参数前的int不能省略: void line(int start, end) { }
带参无返回值的函数声明同样是函数首部加分号,一般格式如下:
void 函数名( 形参变量1, [ 形参变量2,......]);
在调用带参数的函数时,要确保每个形参变量都能得到指定类型的数据,数据的形式不限,可以是常量数据,可以是变量数据,还可以是表达式数据。无返回值带参的函数调用一般格式如下:
函数名(实参值1[,实参值2......]); 【例5.2 line函数的调用示例】 int s=10,e=20;
① line( 10,30); //输出9个空格后,从10位置开始输出21个星号,回车结束 ② line( s,e); //输出9个空格后,从10位置开始输出11个星号,回车结束 ③ line( s,s+24); //输出9个空格后,从10位置开始输出25个星号,回车结束 函数的调用,其实质完成了将实参值赋给形参变量的操作如图5-4所示:
图5-4 参数的传递示意图
思考:如果要在11的位置输出一个星号,是调用line函数还是point函数?
下面使用line函数和point函数重新实现例5-1的triangle函数,设置三角形向右平移8个空格,函数定义如下: void triangle(void) {
line(11,11); point(10,12); line(9,13); }
实现绘制空心三角形的完整程序: 【例5.3 sample05_02.cpp】 /* 打印一个三角形*/ #include
图5-5 空心三角形
/*
*画一条从start开始到end结束的由*组成的线段 */
void line(intstart,int end ) ;
void point(intstart,int end ) ;
void triangle(void); int main() {
printf("打印一个三角形\n"); //函数调用 triangle();
printf("打印结束\n"); return 0; } /*
*画一个空心三角形 */
void triangle(void) {
line(11,11);
void line(intstart,int end )
{ inti;
for(i=1;i
printf(" ");
for(i=start;i
printf("*");
printf("\n"); //回车
}
/*
*画一条从start到end的两端星号中间空格的线段 */
void point(intstart,int end ) {
inti;
for(i=1;i
printf(" "); printf("*");//开始星号
for(i=start+1;i
printf(" ");//结束星号 printf("*\n") ; //回车 }
2
point(10,12); line(9,13);
}
1
与例5-1比较,这个程序中增加了两个用户自定义函数line和point,调用这两个函数重写了triangle函数,输出了一个空心三角形。main函数不需要作改动。
3. 输出可变的三角形
最后来实现三角形的变化,输入一个n值,n值不同,输出三角形的大小也随之变化。 (1)问题分析
修改triangle函数的功能定义,给triangle函数增加一个形参变量n,三角形的大小由n值确定,一共n行,每行2*i-1 个星号。这样,通过增加形参,triangle函数可处理的三角形增多,通用性增强。 (2)算法设计
上例中triangle函数的三句代码标号如下,分析每一句语句的作用, ①line(11,11); ②point(10,12); ③line(9,13);
语句①的作用是画三角形的最上面的一个顶点;语句②的作用画三角形的中部空心线,从顶点向下,每行的空心线开始端减1,结束端加1;语句③的作用是画三角形的底部实心
线,画2n-1个星号。
设定:三角形上方顶点位置middle(最后一行的中间位置),线段开始端s,结束端e。算法设计如下:
1.调用line函数画顶点星号,middle位置是(2n-1)/2+1 2.设置s为middle-1,e为middle+1 3.画n-2条三角形的中部空心线 循环i从2到n-1 ,步长1
3.1 调用point函数画从s到e的空心线 3.2 s减1,e增1
4. 调用line函数画从s到e的实线。
(3)编写程序
triangle函数的实现代码 void triangle(int n ) {
int middle=(2*n-1)/2+1; // middle是最后一行的中间位置 ints,e;
line(middle,middle);// 画顶点星号
s=middle-1; //从顶点向下,每行的空心线开始端减1,结束端加1 e=middle+1;
for(int i=2;i
point(s,e); s--;e++; }
line(s,e); //是画三角形的底部实心线 }
main函数调用时,增加输入n值输入的语句,调用triangle函数时,传递n值到triangle函数,实现输出可变的空心三角形。 【例5.4 sample05_03.cpp】 /* 打印一个三角形*/ #include
void line(int start,int end ) ; void point(int start,int end ) ; void triangle(int n); int main() {int n;
printf("打印一个三角形\n"); printf("n="); scanf("%d",&n); triangle(n);
printf("打印结束\n"); ;
return 0; }
void triangle(int n ) { 略 }
void line(intstart,int end ) { 略}
void point(intstart,int end ) { 略}
任务2计算生日相差几天
任务描述
班主任想知道班上某两位同学在本年度的生日差几天,设计程序,任意输入两个人在本年度的生日日期,能够计算两人的生日差几天。要求使用模块化设计的方法完成任务。
任务解决
一个多函数结构的
C程序是模块化设计的结果,模块化设计的思想实际上是一种“分而治之”的思想,把一个大任务分为若干个子任务,每一个子任务就相对简单了。在拿到一个程序模块以后,根据程序模块的功能将它划分为若干个子模块,如果这些子模块的规模还嫌大,还再可以划分为更小的模块。这个过程采用自顶向下的方法来实现。
任务解决路径:
图5-6 任务2 解决路径
第一步 学习模块化的设计方法得到程序的函数构成 第二步 学习带参有返回值函数的定义
第三步 学习带参有返回值函数的声明和调用
1.模块化设计
计算生日日期之间的差值,可以分别先算出该日期是该年的第几天,再求两者的差值即可。第一层得到的算法如下:
1. 输入本年度year
2. 输入两个同学的生日日期(m1-d1,m2-d2)
3. 计算第一个同学的生日是本年的第几天days1 4. 计算第二个同学的生日是本年的第几天days2 5. 计算并输出days1和days的差值
在第一层的算法分析中,第3、4步还需要细化,解决求一个日期是该年的第几天的问题。例如求2012年4月5日是2012年的第几天,通常的做法是前3个月的总的天数相加,再加上4月份的5天。每个月的总的天数分三种情况:有31天的,有30天的,2月份单独按闰年和非闰年区分为29天和28天,得到算法如下:
year,month,day表示日期,days表示第几天 1. 将day赋给days
2. 累加从month-1月到1月的总的天数到days 3. 返回days
完成累加从month-1月到1月的总的天数到days,可以利用switch语句的case语句没有break语句,则顺次进入下一case分支的特性来完成累计。2月份要根据是否闰年区分是29天还是28天。
判断一个年份是否是闰年的算法:
如果year能被4整除但不能被100整除,或者能被400整除 则返回1 否则返回0
到此向下细化的任务完成,得到模块结构如图5-7所示
图5-7 任务2 模块结构图
2. 编写有返回值的带参函数
C语言实现模块结构图中模块是通过函数来完成的,任务2包括三个函数,模块1对应的是main函数,模块2和模块3是两个有返回值的函数。模块2是已知一个日期求是该年的第几天;模块3是一个判断函数,判断是否是闰年,返回真或假。由于C语言不提供布尔类型表示真假,通常返回1表示真,返回0表示假。 带参有返回值的函数定义的一般形式为:
函数类型 函数名(数据类型 形参变量1,数据类型 形参变量2„) {
return 表达式; }
return语句的作用是将函数的计算结果返回到函数调用处,表达式的值的类型应与函数类型一致。表达式可以直接是一个常量,也可以是变量,也可以是计算表达式。
在定义函数的时候,函数名应与函数实现的功能相呼应,已知的数据要设计形参变量来接受,计算的结果数据决定函数的类型。
模块3的实现功能是已知一个年份求是否是闰年。形参需要一个,接受年份,int类型。返回值是0
或1,int类型,可以定义函数如下:
intisLeap (int y)
{ return (y%4==0&&y%100!=0 ||y%400==0);}
模块2的实现是已知年月日,求第几天,形参变量需要三个,分别表示年月日,int类型;返回值是第几天,int类型。根据算法可以定义函数如下: intcalcDays(inty,intm,int d) { int days=d; switch(m-1) {
case 12: days+=31; case 11:days+=30; case 10: days+=31; case 9 :days+=30; case 8 : days+=31; case 7 :days+=31; case 6 :days+=30; case 5 : days+=31; case 4 :days+=30; case 3 : days+=31;
case 2 :days+=isLeap(y)?29:28; case 1 : days+=31;} return days; }
3. 调用有返回值的带参函数的形式
调用有返回值的带参函数和调用无返回值的带参函数类似,区别在于需要设计如何接收函数计算的返回值。可接收返回值的位置主要有三种,以调用calcDays函数为例: (1)赋值语句
index=calcDays(2012,4,5); (2)运算表达式
d=calcDays(2012,8,5)- calcDays(2012,4,5); (3)函数的参数
printf(“%d-%d-%d是该年的第几天”,year,month,day,calcDays(year,month,day));
返回值为真和假的判断函数调用位置通常在书写if条件或者是循环条件的位置,例如函数calcDays中在一个条件表达式中调用isLeap函数:
days+=isLeap(y)?29:28;
isLeap(y)根据y是否为闰年返回1或0,1为真,条件表达式的值取?后面的29;0为假,条件表达式的值取:后面的28。条件表达式的值累加给days变量。
完成本任务的完整程序如下所示,粗体部分为函数调用的语句
【例5.5 sample05_04.cpp】
#include #include
intisLeap (int y);
intcalcDays(int y,int m,int d);
int main() {
int year,m1,d1,days1,m2,d2,days2; printf("计算两个生日相差的天数\n");
//输入年份和两个同学的生日日期。 printf("year="); scanf("%d",&year);
printf("第一个同学的生日(mm-dd):"); scanf("%d-%d",&m1,&d1);
printf("第二个同学的生日(mm-dd):"); scanf("%d-%d",&m2,&d2);
//计算同学的生日是本年的第几天。 days1= calcDays(year,m1,d1); days2= calcDays(year,m2,d2);
//计算并输出days1和days的差值
printf("两个生日相差的天数为%d\n",abs(days1-days2));
return 0; }
/*
*求一个日期是该年的第几天 */
intcalcDays(int y,int m,int d) {
int days=d; switch(m-1) {
case 12: days+=31; case 11:days+=30; case 10: days+=31; case 9 :days+=30; case 8 : days+=31; case 7 :days+=31; case 6 :days+=30; case 5 : days+=31; case 4 :days+=30; case 3 : days+=31;
case 2 :days+=isLeap(y)?29:28; case 1 : days+=31;
}
return days; } /*
*判断一个年份是否是闰年 */
int isLeap (int y) {
return (y%4==0&&y%100!=0 ||y%400==0); }
运行示例
图5-8 运行结果
调试错误程序示例
多函数结构的程序查错步骤一般如下:
(1)首先要定位出错的函数。在main函数中,断点设置位置往往在IPO的交界处,以确定是输入的错误,还是处理的错误,还是输出的错误。带参有返回值的函数的调用位置往往是断点的设置位置。
(2)如果处理函数的返回值出错,则在出错的函数中第一句设置断点,确定参数传值是否正确。
(3)如果参数传值正确,则在return语句前设置断点,确定是处理算法出错,还是return语句出错。
(4)如果算法出错,则在算法的合适位置设置断点,单步执行,观察变量的值找出错误。
打开程序文件error05_01.cpp,实现功能与smaple05_04.cpp相同。
#include #include
intcalcDays(int y,int m,int d); int main() {
int year,m1,d1,days1,m2,d2,days2; printf("计算两个生日相差的天数\n"); //输入年份和两个同学的生日日期 printf("year="); scanf("%d",&year);
printf("第一个同学的生日(mm-dd):"); scanf("%d-%d",&m1,&d1);
printf("第二个同学的生日(mm-dd):"); scanf("%d-%d",&m2,&d2);
intcalcDays(int y,int m,int d) { int days=d;
switch(m) { case 12: days+=31; case 11: days+=30; case 10: days+=31; case 9 :days+=30; case 8 :days+=31; case 7 :days+=31; case 6 :days+=30; case 5 :days+=31; case 4 :days+=30; case 3 :days+=31;
case 2 :days+=isLeap(y)?29:28; case 1 :days+=31;
//计算同学的生日是本年的第几天 days1= calcDays(int year,m1,d1); days2= calcDays(int year,m2,d2);
//计算并输出days1和days的差值 printf("
return 0;}
两
个
生
日
相
差
的
天
数
}
int isLeap (int y)
{ if(y%4==0&&y%100!=0 ||y%400==0) return 0; else return 1;}
}
return days;
为%d\n",abs(days1-days2));
先对程序文件进行编译,有编译错误,首先修改编译错误。第一条编译错误指向main
函数的calcDays函数调用语句,函数调用时应给出参数的值(实参),赋给函数定义处的形
修改后重新编译,第一条编译错误指向calcDays函数定义中的case 2
语句。编译错误提示isLeap是没有声明的标示符,isLeap是用户自定的函数,编译系统不能识别一般两种原因,一是没有函数声明,二是拼写错误。
再次编译通过,可以运行,运行结果如图所示,结果相差一天,与结果不符,存在逻辑错误。
图5-8调试运行图(1)
调试逻辑错误,在main函数最后的输出语句前设置断点,按F5调试运行,输入year和两个同学的生日日期后,程序在断点处停下,如图5-9所示,在locals选项卡中观察变量的当前值,year和两个同学的生日日期数据正确,days1和days2的值不正确,第一个同学的生日日期应为当年的第28天,第二个同学的生日日期应为当年的第126天,calcDays计算出错,按F5结束本次运行。
图5-9调试运行图(2)
继续检查calcDays函数的出错原因,calcDays函数的return语句直接return days,没有错误,故可以确定函数算法有误,在switch语句前设置断点。按F5调试运行,输入year和两个同学的生日日期后,程序在断点处停下,如图5-10所示,观察变量的值,calcDays函数的参数y、m、d已正确获得main函数传来数据2012、1、28,days取得初值28。继续单步执行按F10后,跳转到switch语句中的case 1,days增至59,这里产生一个逻辑错误。第一个同学是1月份的,不需要加上一月的最大天数,算法上应累加1~(m-1)个月的最大天数,switch语句的开关表达式应为(m-1)。
图5-10调试运行图(3)
执行菜单命令stop debugging结束本次调试,修改后switch语句的表达式后再次运行仍不对,再次断点调试发现days1的值正确为28,但days2的值错误,为125。单步调试calcDays函数,计算第二个同学的生日时,程序正确地进入case 2,在watch1窗口测试isLeap函数的值发现isLeap的值错误,如图5-11所示。2012年是闰年,isLeap(y)应为1。检查isLeap函数发现逻辑错误,交换两个return语句的位置,再执行程序正确。
图5-11调试运行图(4)
独立任务
任务1改错——计算n!
程序文件error5-2.cpp的功能是调用fact(n) 函数,计算n!。 #include int main(void) { int n; double f; printf("Input n:"); scanf("%d",&n); f=fact(m); //设置调试断点1 printf("%d!=%f\n",n,f); return 0; //设置调试断点2 }
double fact(m) { int i; double product; for(i=1;i
return 0; //设置调试断点3 }
要求:
1、请根据VC6的compile和link错误信息改正错误,使程序可以运行。 (要求:截屏一个错误信息,给出对应的修改语句)
2、请按注释要求设置3个调试断点,纠正程序逻辑错误,使其能正确输出n!。 (1)逻辑错误纠正前的调式结果:
● 执行到断点1处,能显示n变量值的watch窗口截屏:
● 执行到断点3处,能显示i、product变量值的watch窗口截屏: ● 执行到断点2处,能显示f变量值的watch窗口截屏: (2)逻辑错误纠正后的调式结果:
● 执行到断点1处,能显示n变量值的watch窗口截屏:
● 执行到断点3处,能显示i、product变量值的watch窗口截屏: ● 执行到断点2处,能显示f变量值的watch窗口截屏:
任务2改写程序
增加参数可以增加函数的可变性和通用性,请修改sample05_03.cpp:
(1)复制sample05_03.cpp并改名为ex5_1.cpp,请为triangle函数增加一个参数变量,用于指定图形左边距空格数。main函数实现输入一个n输出n个n行的三角形,左边距从3开始,每次增加两个空个。图5-12显示了调用三次triangle函数,分别设置左边距空格数为3,5,7的程序执行结果。
图5-12 设置不同的左边距
(2)复制sample05_03.cpp并改名为ex5_2.cpp,请为triangle、point、line函数,增加一个参数变量指定输出字符,main函数实现输入一个n和字符,输出一个n行的由输入字符构成的三角形,左边距为5。如图5-13所示。
图5-13 可变字符的三角形
(3)复制sample05_03.cpp并改名为ex5_3.cpp,使用point和line函数设计一个绘制可变矩形的函数rectangle,矩形的长和宽、左边的边距可以调用时指定时。例如:一个
高为10,宽为5,边距为5的矩形如图5-13所示。
图5-14 可变的矩形
任务3 编程
(1)打开程序文件ex5_4.cpp,程序的功能是实现在main函数中通过调用getCelsius函数完成华氏温度转化为摄氏温度。请填空完整程序。 #include //声明getCelsius函数
int main(void) {
intfahr;
printf("input fahr:"); scanf("%d",&fahr);
printf("fahr = %d, celsius = %d\n", fahr, getCelsius(fahr)); return 0; }
//定义函数getCelsius { intcelsius; //计算摄氏温度 celsius=
//返回摄氏温度 }
(2)打开程序文件ex5_5.cpp,定义函数double average(intx,inty,int z) 计算3个整数的算术平均值,并在main函数中输入3个整数,调用函数average计算平均值并输出。 (3)打开程序文件ex5_6.cpp,编写函数intgetLastBit(int number),该函数返回正整数number的个位数,例如正整数1234,则返回4。在main函数中实现输入n个任意整数,取n个数的最后一位构成一个新的整数后输出。例如依次输入4个数(45、81、673 938),得到的新的整数为:5318。 #include
intgetLastBit(int number); int main()
{
//1.输入n
//2.新整数初始化为0
//3.循环i从1到n,step 1: //3.1 读入一个整数
//3.2调用getLastBit求解最后一位数 //3.3 将最后一位数构造到新整数 //4.输出结果 }
intgetLastBit(int number) {//返回number的最后一位 }
(4)打开程序文件ex5_7.cpp,程序实现的功能是输出1900年至今所有的闰年,每行10个,要求调用isLeap函数判闰年。 #include int main() {
return 0;}
intisLeap (int y) {
return (y%4==0&&y%100!=0 ||y%400==0); }
实验五 模块化的程序设计
实验目的
(1)理解自顶向下,逐步细化的模块化设计思想划分子模块;知道模块化程序设计方法。 (2)熟悉函数的定义、函数的声明和函数的调用,理解函数调用的实现过程。
(3)理解函数调用时的数据传送机制,通过参数将主调函数的数据传递到被调函数,运用
return语句将被调函数的处理结果返回主调函数。
(4)通过设置断点和单步执行,观察子函数的执行路径,和子函数中变量的值的变化。
实验范例 引导任务
任务1会变化的三角形 任务2 计算生日相差几天
任务1 会变化的三角形
任务描述
在屏幕上打印一个三角形,如图5-1所示,运行程序时输入三角形的级别n,可以打印不同大小的三角形。
图5-1 输出可变的三角形
任务解决
任务解决路径:
图5-2 任务1 解决路径
第一步:学习无参无返回值函数的定义、声明和调用;
第二步:了解函数参数的作用,学习使用带参的函数构建多层模块程序的构建; 第三步:学习带参无返回值函数的设计;
1.编写打印三角形的函数
(1)问题分析
要在屏幕上打印一个三角形,可以直接在main函数中使用printf语句完成,如下程序代码所示:
#include int main() {
printf("打印一个三角形\n ");
printf(" *\n");
printf(" ***\n");
printf("*****\n");
printf("打印结束\n");
return 0; }
在本任务中,要求调整程序的结构,写一个打印三角形的无参无返回值函数,然后在main函数中调用它实现打印三角形。程序员编写的函数称为用户自定义函数,相对于系统函数而言,不是系统已写好的代码,需要程序员自己定义函数和编写实现函数的程序代码。无参无返回值函数通常执行一个具体的操作,无计算结果。 (2)函数的应用
使用用户自定义函数分三步:函数定义,函数声明和函数调用。 ①无参无返回值函数的定义
无参无返回值函数定义的一般形式为:
void函数名(void) { ...... }
一个打印三角形的函数的定义可以为:
void triangle(void) { printf(" *\n"); printf(" ***\n"); printf("*****\n"); }
triangle是函数名,函数名应能体现函数的功能,以增强程序的可读性,这个函数的功能是打印一个固定的三角形。函数类型为void,表示没有返回值,形参表中写void,表示没有参数,void可以省略,但一对圆括号不能省略,表示函数运算符。 ② 函数的声明
用户自定义函数要先声明后调用。无参无返回值的函数声明的一般形式:
void 函数名(void);
例如triangle函数的声明语句为: void triangle(void);
函数声明所需要的信息都在函数的首部,书写函数声明时可以复制函数首部,加分号。 ③ 函数调用
无返回值函数的调用形式为: 函数名();
例如triangle函数的调用语句为:
triangle();
虽然triangle函数没有参数,实参列表为空,但一对圆括号不能省略,而且圆括号中不能写void。这种从函数名开始的调用形式,称为函数语句。
(3)编写程序
最后关注这三个组成部分在程序中出现的位置,程序sample05_01.cpp演示了一个规范的程序代码格式。
【例5.1 sample05_01.cpp】 /* 打印一个三角形*/ #include //函数声明
void triangle(void);
int main() {
printf("打印一个三角形\n"); //函数调用
triangle();
printf("打印结束\n"); ; return 0;} //函数定义
void triangle(void) {
printf(" *\n"); printf(" ***\n"); printf("*****\n"); }
在程序中,函数定义的位置最好在main函数后面,因为程序从main函数开始执行,
到main函数的最后一句语句结束,我们阅读程序也习惯从main函数开始。函数和函数之间没有先后关系,也没有包含关系,不能在一个函数的内部定义另一个函数,每个函数都是独立定义的。
函数声明通常放在预编译命令的后面,main函数之前。这是一个全局位置,函数声明对所有的函数有效。如果函数声明放在另一个函数的内部,那只对该函数有效。
函数调用按程序的需要进行,一个函数定义并声明后,可以多次调用。程序的执行从main函数的第一条可执行语句开始,执行完printf语句,输出字符串“打印一个三角形”,顺次执行函数调用语句“triangle();”,程序转向triangle的函数定义处,执行triangle函数中的语句,执行完后回到main函数,接着向下执行printf语句和return语句,main执行结束,程序结束。
思考:如果要在垂直方向上连续输出三个三角形,如图所示,该如何修改你的程序呢?
图5-3 输出三个三角形
提示:函数是通用项目,可以多次调用。
2. 使用通用函数编写三角形函数
先阅读两个通用功能函数line和point,它们实现基本的显示线段功能。 line函数定义如下:
void line(intstart,int end ) line函数的作用是画一条从start开始{ inti; 到end结束的由*组成的线段,start和end for(i=1;i
printf(" ");
printf("*\n") ; //结束星号和回车 }
line函数和point函数与前面的triangle函数相比,增加了两个参数变量start和end,这两个变量在函数调用时可以接受两个整数,决定画出的线段的长短。参数变量定义的方法与一般函数体内定义变量的方法相似,要设定变量的数据类型,但不同的是每个参数变量必须单独定义。带参无返回值的函数定义一般格式如下:
void 函数名( 形参变量1, [ 形参变量2,......]) {
....... }
line函数的两个参数变量都是整型,也不能共用一个int类型,下面的定义形式是错误的,end参数前的int不能省略: void line(int start, end) { }
带参无返回值的函数声明同样是函数首部加分号,一般格式如下:
void 函数名( 形参变量1, [ 形参变量2,......]);
在调用带参数的函数时,要确保每个形参变量都能得到指定类型的数据,数据的形式不限,可以是常量数据,可以是变量数据,还可以是表达式数据。无返回值带参的函数调用一般格式如下:
函数名(实参值1[,实参值2......]); 【例5.2 line函数的调用示例】 int s=10,e=20;
① line( 10,30); //输出9个空格后,从10位置开始输出21个星号,回车结束 ② line( s,e); //输出9个空格后,从10位置开始输出11个星号,回车结束 ③ line( s,s+24); //输出9个空格后,从10位置开始输出25个星号,回车结束 函数的调用,其实质完成了将实参值赋给形参变量的操作如图5-4所示:
图5-4 参数的传递示意图
思考:如果要在11的位置输出一个星号,是调用line函数还是point函数?
下面使用line函数和point函数重新实现例5-1的triangle函数,设置三角形向右平移8个空格,函数定义如下: void triangle(void) {
line(11,11); point(10,12); line(9,13); }
实现绘制空心三角形的完整程序: 【例5.3 sample05_02.cpp】 /* 打印一个三角形*/ #include
图5-5 空心三角形
/*
*画一条从start开始到end结束的由*组成的线段 */
void line(intstart,int end ) ;
void point(intstart,int end ) ;
void triangle(void); int main() {
printf("打印一个三角形\n"); //函数调用 triangle();
printf("打印结束\n"); return 0; } /*
*画一个空心三角形 */
void triangle(void) {
line(11,11);
void line(intstart,int end )
{ inti;
for(i=1;i
printf(" ");
for(i=start;i
printf("*");
printf("\n"); //回车
}
/*
*画一条从start到end的两端星号中间空格的线段 */
void point(intstart,int end ) {
inti;
for(i=1;i
printf(" "); printf("*");//开始星号
for(i=start+1;i
printf(" ");//结束星号 printf("*\n") ; //回车 }
2
point(10,12); line(9,13);
}
1
与例5-1比较,这个程序中增加了两个用户自定义函数line和point,调用这两个函数重写了triangle函数,输出了一个空心三角形。main函数不需要作改动。
3. 输出可变的三角形
最后来实现三角形的变化,输入一个n值,n值不同,输出三角形的大小也随之变化。 (1)问题分析
修改triangle函数的功能定义,给triangle函数增加一个形参变量n,三角形的大小由n值确定,一共n行,每行2*i-1 个星号。这样,通过增加形参,triangle函数可处理的三角形增多,通用性增强。 (2)算法设计
上例中triangle函数的三句代码标号如下,分析每一句语句的作用, ①line(11,11); ②point(10,12); ③line(9,13);
语句①的作用是画三角形的最上面的一个顶点;语句②的作用画三角形的中部空心线,从顶点向下,每行的空心线开始端减1,结束端加1;语句③的作用是画三角形的底部实心
线,画2n-1个星号。
设定:三角形上方顶点位置middle(最后一行的中间位置),线段开始端s,结束端e。算法设计如下:
1.调用line函数画顶点星号,middle位置是(2n-1)/2+1 2.设置s为middle-1,e为middle+1 3.画n-2条三角形的中部空心线 循环i从2到n-1 ,步长1
3.1 调用point函数画从s到e的空心线 3.2 s减1,e增1
4. 调用line函数画从s到e的实线。
(3)编写程序
triangle函数的实现代码 void triangle(int n ) {
int middle=(2*n-1)/2+1; // middle是最后一行的中间位置 ints,e;
line(middle,middle);// 画顶点星号
s=middle-1; //从顶点向下,每行的空心线开始端减1,结束端加1 e=middle+1;
for(int i=2;i
point(s,e); s--;e++; }
line(s,e); //是画三角形的底部实心线 }
main函数调用时,增加输入n值输入的语句,调用triangle函数时,传递n值到triangle函数,实现输出可变的空心三角形。 【例5.4 sample05_03.cpp】 /* 打印一个三角形*/ #include
void line(int start,int end ) ; void point(int start,int end ) ; void triangle(int n); int main() {int n;
printf("打印一个三角形\n"); printf("n="); scanf("%d",&n); triangle(n);
printf("打印结束\n"); ;
return 0; }
void triangle(int n ) { 略 }
void line(intstart,int end ) { 略}
void point(intstart,int end ) { 略}
任务2计算生日相差几天
任务描述
班主任想知道班上某两位同学在本年度的生日差几天,设计程序,任意输入两个人在本年度的生日日期,能够计算两人的生日差几天。要求使用模块化设计的方法完成任务。
任务解决
一个多函数结构的
C程序是模块化设计的结果,模块化设计的思想实际上是一种“分而治之”的思想,把一个大任务分为若干个子任务,每一个子任务就相对简单了。在拿到一个程序模块以后,根据程序模块的功能将它划分为若干个子模块,如果这些子模块的规模还嫌大,还再可以划分为更小的模块。这个过程采用自顶向下的方法来实现。
任务解决路径:
图5-6 任务2 解决路径
第一步 学习模块化的设计方法得到程序的函数构成 第二步 学习带参有返回值函数的定义
第三步 学习带参有返回值函数的声明和调用
1.模块化设计
计算生日日期之间的差值,可以分别先算出该日期是该年的第几天,再求两者的差值即可。第一层得到的算法如下:
1. 输入本年度year
2. 输入两个同学的生日日期(m1-d1,m2-d2)
3. 计算第一个同学的生日是本年的第几天days1 4. 计算第二个同学的生日是本年的第几天days2 5. 计算并输出days1和days的差值
在第一层的算法分析中,第3、4步还需要细化,解决求一个日期是该年的第几天的问题。例如求2012年4月5日是2012年的第几天,通常的做法是前3个月的总的天数相加,再加上4月份的5天。每个月的总的天数分三种情况:有31天的,有30天的,2月份单独按闰年和非闰年区分为29天和28天,得到算法如下:
year,month,day表示日期,days表示第几天 1. 将day赋给days
2. 累加从month-1月到1月的总的天数到days 3. 返回days
完成累加从month-1月到1月的总的天数到days,可以利用switch语句的case语句没有break语句,则顺次进入下一case分支的特性来完成累计。2月份要根据是否闰年区分是29天还是28天。
判断一个年份是否是闰年的算法:
如果year能被4整除但不能被100整除,或者能被400整除 则返回1 否则返回0
到此向下细化的任务完成,得到模块结构如图5-7所示
图5-7 任务2 模块结构图
2. 编写有返回值的带参函数
C语言实现模块结构图中模块是通过函数来完成的,任务2包括三个函数,模块1对应的是main函数,模块2和模块3是两个有返回值的函数。模块2是已知一个日期求是该年的第几天;模块3是一个判断函数,判断是否是闰年,返回真或假。由于C语言不提供布尔类型表示真假,通常返回1表示真,返回0表示假。 带参有返回值的函数定义的一般形式为:
函数类型 函数名(数据类型 形参变量1,数据类型 形参变量2„) {
return 表达式; }
return语句的作用是将函数的计算结果返回到函数调用处,表达式的值的类型应与函数类型一致。表达式可以直接是一个常量,也可以是变量,也可以是计算表达式。
在定义函数的时候,函数名应与函数实现的功能相呼应,已知的数据要设计形参变量来接受,计算的结果数据决定函数的类型。
模块3的实现功能是已知一个年份求是否是闰年。形参需要一个,接受年份,int类型。返回值是0
或1,int类型,可以定义函数如下:
intisLeap (int y)
{ return (y%4==0&&y%100!=0 ||y%400==0);}
模块2的实现是已知年月日,求第几天,形参变量需要三个,分别表示年月日,int类型;返回值是第几天,int类型。根据算法可以定义函数如下: intcalcDays(inty,intm,int d) { int days=d; switch(m-1) {
case 12: days+=31; case 11:days+=30; case 10: days+=31; case 9 :days+=30; case 8 : days+=31; case 7 :days+=31; case 6 :days+=30; case 5 : days+=31; case 4 :days+=30; case 3 : days+=31;
case 2 :days+=isLeap(y)?29:28; case 1 : days+=31;} return days; }
3. 调用有返回值的带参函数的形式
调用有返回值的带参函数和调用无返回值的带参函数类似,区别在于需要设计如何接收函数计算的返回值。可接收返回值的位置主要有三种,以调用calcDays函数为例: (1)赋值语句
index=calcDays(2012,4,5); (2)运算表达式
d=calcDays(2012,8,5)- calcDays(2012,4,5); (3)函数的参数
printf(“%d-%d-%d是该年的第几天”,year,month,day,calcDays(year,month,day));
返回值为真和假的判断函数调用位置通常在书写if条件或者是循环条件的位置,例如函数calcDays中在一个条件表达式中调用isLeap函数:
days+=isLeap(y)?29:28;
isLeap(y)根据y是否为闰年返回1或0,1为真,条件表达式的值取?后面的29;0为假,条件表达式的值取:后面的28。条件表达式的值累加给days变量。
完成本任务的完整程序如下所示,粗体部分为函数调用的语句
【例5.5 sample05_04.cpp】
#include #include
intisLeap (int y);
intcalcDays(int y,int m,int d);
int main() {
int year,m1,d1,days1,m2,d2,days2; printf("计算两个生日相差的天数\n");
//输入年份和两个同学的生日日期。 printf("year="); scanf("%d",&year);
printf("第一个同学的生日(mm-dd):"); scanf("%d-%d",&m1,&d1);
printf("第二个同学的生日(mm-dd):"); scanf("%d-%d",&m2,&d2);
//计算同学的生日是本年的第几天。 days1= calcDays(year,m1,d1); days2= calcDays(year,m2,d2);
//计算并输出days1和days的差值
printf("两个生日相差的天数为%d\n",abs(days1-days2));
return 0; }
/*
*求一个日期是该年的第几天 */
intcalcDays(int y,int m,int d) {
int days=d; switch(m-1) {
case 12: days+=31; case 11:days+=30; case 10: days+=31; case 9 :days+=30; case 8 : days+=31; case 7 :days+=31; case 6 :days+=30; case 5 : days+=31; case 4 :days+=30; case 3 : days+=31;
case 2 :days+=isLeap(y)?29:28; case 1 : days+=31;
}
return days; } /*
*判断一个年份是否是闰年 */
int isLeap (int y) {
return (y%4==0&&y%100!=0 ||y%400==0); }
运行示例
图5-8 运行结果
调试错误程序示例
多函数结构的程序查错步骤一般如下:
(1)首先要定位出错的函数。在main函数中,断点设置位置往往在IPO的交界处,以确定是输入的错误,还是处理的错误,还是输出的错误。带参有返回值的函数的调用位置往往是断点的设置位置。
(2)如果处理函数的返回值出错,则在出错的函数中第一句设置断点,确定参数传值是否正确。
(3)如果参数传值正确,则在return语句前设置断点,确定是处理算法出错,还是return语句出错。
(4)如果算法出错,则在算法的合适位置设置断点,单步执行,观察变量的值找出错误。
打开程序文件error05_01.cpp,实现功能与smaple05_04.cpp相同。
#include #include
intcalcDays(int y,int m,int d); int main() {
int year,m1,d1,days1,m2,d2,days2; printf("计算两个生日相差的天数\n"); //输入年份和两个同学的生日日期 printf("year="); scanf("%d",&year);
printf("第一个同学的生日(mm-dd):"); scanf("%d-%d",&m1,&d1);
printf("第二个同学的生日(mm-dd):"); scanf("%d-%d",&m2,&d2);
intcalcDays(int y,int m,int d) { int days=d;
switch(m) { case 12: days+=31; case 11: days+=30; case 10: days+=31; case 9 :days+=30; case 8 :days+=31; case 7 :days+=31; case 6 :days+=30; case 5 :days+=31; case 4 :days+=30; case 3 :days+=31;
case 2 :days+=isLeap(y)?29:28; case 1 :days+=31;
//计算同学的生日是本年的第几天 days1= calcDays(int year,m1,d1); days2= calcDays(int year,m2,d2);
//计算并输出days1和days的差值 printf("
return 0;}
两
个
生
日
相
差
的
天
数
}
int isLeap (int y)
{ if(y%4==0&&y%100!=0 ||y%400==0) return 0; else return 1;}
}
return days;
为%d\n",abs(days1-days2));
先对程序文件进行编译,有编译错误,首先修改编译错误。第一条编译错误指向main
函数的calcDays函数调用语句,函数调用时应给出参数的值(实参),赋给函数定义处的形
修改后重新编译,第一条编译错误指向calcDays函数定义中的case 2
语句。编译错误提示isLeap是没有声明的标示符,isLeap是用户自定的函数,编译系统不能识别一般两种原因,一是没有函数声明,二是拼写错误。
再次编译通过,可以运行,运行结果如图所示,结果相差一天,与结果不符,存在逻辑错误。
图5-8调试运行图(1)
调试逻辑错误,在main函数最后的输出语句前设置断点,按F5调试运行,输入year和两个同学的生日日期后,程序在断点处停下,如图5-9所示,在locals选项卡中观察变量的当前值,year和两个同学的生日日期数据正确,days1和days2的值不正确,第一个同学的生日日期应为当年的第28天,第二个同学的生日日期应为当年的第126天,calcDays计算出错,按F5结束本次运行。
图5-9调试运行图(2)
继续检查calcDays函数的出错原因,calcDays函数的return语句直接return days,没有错误,故可以确定函数算法有误,在switch语句前设置断点。按F5调试运行,输入year和两个同学的生日日期后,程序在断点处停下,如图5-10所示,观察变量的值,calcDays函数的参数y、m、d已正确获得main函数传来数据2012、1、28,days取得初值28。继续单步执行按F10后,跳转到switch语句中的case 1,days增至59,这里产生一个逻辑错误。第一个同学是1月份的,不需要加上一月的最大天数,算法上应累加1~(m-1)个月的最大天数,switch语句的开关表达式应为(m-1)。
图5-10调试运行图(3)
执行菜单命令stop debugging结束本次调试,修改后switch语句的表达式后再次运行仍不对,再次断点调试发现days1的值正确为28,但days2的值错误,为125。单步调试calcDays函数,计算第二个同学的生日时,程序正确地进入case 2,在watch1窗口测试isLeap函数的值发现isLeap的值错误,如图5-11所示。2012年是闰年,isLeap(y)应为1。检查isLeap函数发现逻辑错误,交换两个return语句的位置,再执行程序正确。
图5-11调试运行图(4)
独立任务
任务1改错——计算n!
程序文件error5-2.cpp的功能是调用fact(n) 函数,计算n!。 #include int main(void) { int n; double f; printf("Input n:"); scanf("%d",&n); f=fact(m); //设置调试断点1 printf("%d!=%f\n",n,f); return 0; //设置调试断点2 }
double fact(m) { int i; double product; for(i=1;i
return 0; //设置调试断点3 }
要求:
1、请根据VC6的compile和link错误信息改正错误,使程序可以运行。 (要求:截屏一个错误信息,给出对应的修改语句)
2、请按注释要求设置3个调试断点,纠正程序逻辑错误,使其能正确输出n!。 (1)逻辑错误纠正前的调式结果:
● 执行到断点1处,能显示n变量值的watch窗口截屏:
● 执行到断点3处,能显示i、product变量值的watch窗口截屏: ● 执行到断点2处,能显示f变量值的watch窗口截屏: (2)逻辑错误纠正后的调式结果:
● 执行到断点1处,能显示n变量值的watch窗口截屏:
● 执行到断点3处,能显示i、product变量值的watch窗口截屏: ● 执行到断点2处,能显示f变量值的watch窗口截屏:
任务2改写程序
增加参数可以增加函数的可变性和通用性,请修改sample05_03.cpp:
(1)复制sample05_03.cpp并改名为ex5_1.cpp,请为triangle函数增加一个参数变量,用于指定图形左边距空格数。main函数实现输入一个n输出n个n行的三角形,左边距从3开始,每次增加两个空个。图5-12显示了调用三次triangle函数,分别设置左边距空格数为3,5,7的程序执行结果。
图5-12 设置不同的左边距
(2)复制sample05_03.cpp并改名为ex5_2.cpp,请为triangle、point、line函数,增加一个参数变量指定输出字符,main函数实现输入一个n和字符,输出一个n行的由输入字符构成的三角形,左边距为5。如图5-13所示。
图5-13 可变字符的三角形
(3)复制sample05_03.cpp并改名为ex5_3.cpp,使用point和line函数设计一个绘制可变矩形的函数rectangle,矩形的长和宽、左边的边距可以调用时指定时。例如:一个
高为10,宽为5,边距为5的矩形如图5-13所示。
图5-14 可变的矩形
任务3 编程
(1)打开程序文件ex5_4.cpp,程序的功能是实现在main函数中通过调用getCelsius函数完成华氏温度转化为摄氏温度。请填空完整程序。 #include //声明getCelsius函数
int main(void) {
intfahr;
printf("input fahr:"); scanf("%d",&fahr);
printf("fahr = %d, celsius = %d\n", fahr, getCelsius(fahr)); return 0; }
//定义函数getCelsius { intcelsius; //计算摄氏温度 celsius=
//返回摄氏温度 }
(2)打开程序文件ex5_5.cpp,定义函数double average(intx,inty,int z) 计算3个整数的算术平均值,并在main函数中输入3个整数,调用函数average计算平均值并输出。 (3)打开程序文件ex5_6.cpp,编写函数intgetLastBit(int number),该函数返回正整数number的个位数,例如正整数1234,则返回4。在main函数中实现输入n个任意整数,取n个数的最后一位构成一个新的整数后输出。例如依次输入4个数(45、81、673 938),得到的新的整数为:5318。 #include
intgetLastBit(int number); int main()
{
//1.输入n
//2.新整数初始化为0
//3.循环i从1到n,step 1: //3.1 读入一个整数
//3.2调用getLastBit求解最后一位数 //3.3 将最后一位数构造到新整数 //4.输出结果 }
intgetLastBit(int number) {//返回number的最后一位 }
(4)打开程序文件ex5_7.cpp,程序实现的功能是输出1900年至今所有的闰年,每行10个,要求调用isLeap函数判闰年。 #include int main() {
return 0;}
intisLeap (int y) {
return (y%4==0&&y%100!=0 ||y%400==0); }