C语言模块化程序设计1王新余.docx
《C语言模块化程序设计1王新余.docx》由会员分享,可在线阅读,更多相关《C语言模块化程序设计1王新余.docx(33页珍藏版)》请在冰豆网上搜索。
![C语言模块化程序设计1王新余.docx](https://file1.bdocx.com/fileroot1/2023-5/21/cea2f874-25a9-4c8c-a7a4-baf15756a31f/cea2f874-25a9-4c8c-a7a4-baf15756a31f1.gif)
C语言模块化程序设计1王新余
模块化程序设计
一、概述
程序模块化的发展过程:
(1)开始,软件是一大块机器代码。
(2)程序库,为不同程序提供公共服务。
(3)独立地址空间和进程间通信。
(4)程序系统分布在不同的互联的主机上。
本节课,先讲述程序库,即函数级,C语言程序模块化的基础知识和注意点。
再讲述RMX上多进程的特点。
·程序设计方法:
自上而下,逐步细化,模块化设计,结构化编程
·原则:
模块间耦合越小越好
·C语言:
函数式语言
在C程序设计中,通常:
·将一个大程序分成几个子程序模块(自定义函数)
·将常用功能做成标准模块(标准函数)放在函数库中供其他程序调用
如果把编程比做制造一台机器,函数就好比其零部件。
·可将这些“零部件”单独设计、调试、测试好,用时拿出来装配,再总体调试。
·这些“零部件”可以是自己设计制造/别人设计制造/现在的标准产品
而且,许多“零部件”我们可以只知道需向它提供什么(如控制信号),它能产生什么(如速度/动力),并不需要了解它是如何工作、如何设计制造的——所谓“黑盒子”。
控制信号速度/动力
(输入参数)(返回结果)
【例】编写一个儿童算术能力测试软件
显示软件封面
检查密码
产生题目
接受回答
评判计分
显示结果
如果要继续练习
告别词
main(){
charans=‘y’;
clrscr();
cover();/*调用软件封面显示函数*/
password();/*调用密码检查函数*/
while(ans==’y’||ans==’Y’)
{question();/*调用产生题目函数*/
answers();/*调用接受回答函数*/
marks();/*调用评分函数*/
results(); /*调用结果显示函数*/
printf(“是否继续练习?
(Y/N)\n”);
ans=getch();
}
printf(“谢谢使用,再见!
”);
}
/*定义所用函数*/
cover(){}/*软件封面显示函数*/
password(){}/*密码检查函数*/
question(){}/*产生题目函数*/
answers(){}/*接受回答函数*/
marks(){}/*评分函数*/
results(){}/*结果显示函数*/
函数使用常识:
1、C程序执行总是从main函数开始,调用其它函数后总是回到main函数,最后在main函数中结束整个程序的运行。
2、一个C程序由一个或多个源(程序)文件组成——可分别编写、编译和调试。
3、一个源文件由一个或多个函数组成,可为多个C程序公用。
4、C语言是以源文件为单位而不以函数为单位进行编译的。
5、所有函数都是平行的、互相独立的,即在一个函数内只能调用其他函数,不能再定义一个函数(嵌套定义)。
6、一个函数可以调用其他函数或其本身,但任何函数均不可调用main函数。
二、函数的定义
函数定义——“制造函数”
1、无参函数
定义格式:
数据类型函数名()/*现代风格是:
函数名(void)*/
{函数体(说明部分+语句)}
【注意】数据类型为int时,可以省略。
2、有参函数
定义格式:
数据类型函数名(形参表)/*现代风格是:
函数名(带类型形参表)*/
形参类型说明;
{函数体(说明部分+语句)}
函数的返回值通过函数体中的return语句获得。
形式:
return(x);return(x+y);return(x>y?
x:
y);
语句中圆括号亦可省略。
【注意】如果函数值类型与return语句表达式值的类型不一致,以函数类型为准(数值型会自动进行类型转换)。
如果明确表示不需返回值,可用void作函数的数据类型。
3、 空函数
类型说明符函数名()
{}
空函数,什么工作也不作,没有任何实际作用,只表明“这里要调用一个函数”,而现在这个函数没有起作用,等以后扩充函数功能时补上。
这样做可使程序结构清晰,可读写好,以后扩充新功能方便,对程序结构影响不大。
【例一】
max(floatx,floaty)/*用“值传递”分析法进行变量跟踪*/
{
floatz;
z=x>y?
x:
y;
returnz;
}
main()
{
floata=1.5,b=2.5;
intc;
c=max(a,b);
printf(“Maxis%d\n”,c);
}
结果:
Maxis2(编译通过,结果错误)
【讨论】如何改错?
·如果将输出语句中的%d改为%f,结果:
Maxis8.000000(亦错)
·如果将max(x,y)改为floatmax(x,y),结果编译出错:
Typemismatchinredeclarationof‘max’
三、函数参数和函数的值
1、形参和实参
在调用有参函数时,主调函数和被调函数之间有数据传递关系。
在定义函数名后面括号中的变量名称为“形式参数”,在调用函数时,函数名后面括号中的表达式称为“实际参数”。
关于形参与实参的说明:
·形参在未出现函数调用时,不占用内存。
只有在函数被调用时,形参才被分配内存单元,调用结束后,形参所占的内存单元自动释放。
·实参可以是常量、变量或表达式,但要求有确定的值。
在调用时,把实参的值赋给形参变量(如果形参是数组名,则传递的是数组首地址)。
·在被定义的函数中,必须指定形参的类型。
·实参与形参类型应一致。
·C语言规定,实参变量对形参变量的数据传递是“值传递”,即单向传递,只有实参传给形参,而不能由形参传回给实参。
在内存中,实参与形参是不同的内存单元。
【例一】
voidGetMemory(char*p,intlength)
{
p=(char*)malloc(length);
}
voidTest(void)
{
char*ptr=NULL;
GetMemory(ptr,10);
Strcpy(ptr,”hello”);
}
2、数组名作为函数参数
·用数组名作为函数参数,此时实参与形参都应用数组名(或用数组指针),应该在主调函数和被调函数分别定义数组。
·实参数组和形参数组的类型应一致。
·实参数组和形参数组的大小可以不一致,C编译对形参数组大小不作检查,只是将实参数组的首地址传给形参数组。
形参数组也可以不指定大小,在定义数组时在数组名后面跟一个空的方括弧。
·数组名作为函数参数时,不是“值传递”,不是单向传递,而是把实参数组的起始地址传给形参数组,两个数组共同占用一段内存单元。
【例二】
floataverage(floatarray〔〕,intn)
{
}
main()
{
floatscore〔5〕;
average(score,5);
}
在此例中,main函数调用average时,是把数组score地址压入堆栈,array指向数组score〔〕的内存单元。
·多维数组作为函数参数,形参只能省略第一维的大小说明。
在C语言中,数组在内存中按行列规则存放,先行后列,如果形参中不说明列数,系统无法决定多少行多少列。
3、函数的返回值
·函数的返回值是通过函数中的return语句获得。
·函数定义指定函数值类型。
C语言规定,凡不加类型说明的函数,一律自动按整形处理。
·如果函数值的类型和return语句表达式的值不一致,以函数类型为准。
·如果被调用函数中没有return语句,函数并不是不带回值,而是带回一部确定的值。
C语言中,普通数据类型用寄存器eax带回返回值,指针类型用寄存器dx带回段地址,寄存器eax带回偏移地址。
如果被调用函数中没有return语句,则返回值就是在函数运行结束时寄存器eax和dx的值。
·为了明确函数“不带回值”,用“void”定义“无类型”。
四、函数的调用
函数和变量一样,在其主调函数中也必须“先说明,后使用”。
注意关系:
函数定义——制造函数
函数使用——说明(准备使用)
调用(使用函数)
调用方式:
·赋值如:
c=max(x,y);
·表达式中c=1+max(x,y);printf(“Max=%d\n”,max(x,y));
·执行函数max(x,y);
1、调用外部函数(其他源文件中定义的函数)时
函数说明语句extern〔函数类型〕函数名();
【例】文件file1.c中
main()
{
Intx=80,y=90,c;
externmax();/*函数说明*/
c=max(x,y)+20;/*调用max函数*/
printf(“Maxis%d\n”,c);
}
文件files2.c中(与file1.c同目录)
externmax(inta,intb)/*extern可省*/
{
floatc;
c=a>b?
a:
b;
returnc;
}
2、调用同一源文件中的非标准函数时
也必须在主调函数中对所调函数进行说明:
函数说明语句数据类型函数名();(不说明会出现编译错误)
但三种情况下可以省略说明:
①函数值是整型(int)或字符型(char)时——系统自动按整型说明;
②所调函数的定义出现在主调函数之前时;
③文件一开头,在所有函数之前,对所用函数作了说明
3、通过函数指针调用
4、函数的嵌套调用
C语言的函数定义都是相互平行、独立的,也就是在定义函数时,一个函数内不能包含另一个函数。
C运用可以嵌套定义数据类型和嵌套调用函数,
五、函数的递归调用
1、递归的概念
直接递归调用调用函数的过程中又调用该函数本身
间接递归调用调用f1函数的过程中调用f2函数,而f2中又需要调用f1。
以上均为无终止递归调用。
为此,一般要用if语句来控制使递归过程到某一条件满足时结束。
2、递归法
类似于数学证明中的反推法,从后一结果与前一结果的关系中寻找其规律性。
归纳法可以分为:
·递推法从初值出发,归纳出新值与旧值间直到最后值为止存在的关系
要求通过分析得到:
初值+递推公式
编程:
通过循环控制结构实现(循环的终值是最后值)
·递归法从结果出发,归纳出后一结果与前一结果直到初值为止存在的关系
要求通过分析得到:
初值+递归函数
编程:
设计一个函数(递归函数),这个函数不断使用下一级值调用自身,直到结果已知处——选择控制结构
其一般形式是:
在主函数中用终值n调用递归函数,而在递归函数中:
递归函数名f(参数x)
{if(n==初值)
结果=…;
else
结果=含f(x-1)的表达式;
返回结果(return);
}
【例一】(P118例7.8)用递归法求n!
分析比较:
递推法
递归法
0!
=1
1!
=0!
×1
2!
=1!
×2
3!
=2!
×3
……
n!
=(n-1)!
×n
分析得Sn=n!
的求解
1(n=1,0)
Sn=
Sn-1×n
(n>1)
其中Sn-1先求出
n!
=(n-1)!
×n
(n-1)!
=(n-2)!
×(n-1)
(n-2)!
=(n-3)!
×(n-2)
(n-3)!
=(n-4)!
×(n-3)
……
2!
=1!
×2
分析得f(n)=n!
的求解
1(n=1,0)
f(n)=
f(n-1)×n
(n>1)
其中f(n-1)未求出
实际上,递归程序分两个阶段执行——
①回推(调用):
欲求n!
→先求(n-1)!
→(n-2)!
→…→1!
若1!
已知,回推结束。
②递推(回代):
知道1!
→2!
可求出→3!
→…→n!
程序如下:
main()
{
intn;
floats;
floatfac();
clrscr();
printf("Inputn=");
scanf("%d",&n);
s=fac(n);
printf("%d!
=%.0f",n,s);
}
floatfac(intx)
{intf;
if(x==0||x==1)f=1;
elsef=fac(x-1)*x;
returnf;
}
执行:
Inputn=5
结果:
5!
=120
【例二】(P116例7.7)有5个人,第5个人说他比第4个人大2岁,第4个人说他对第3个人大2岁,第3个人说他对第2个人大2岁,第2个人说他比第1个人大2岁,第1个人说他10岁。
求第5个人多少岁。
分析:
10(n=1)
age(n)=age(n-1)+2(n>1)
程序如下:
main()
{clrscr();
printf("%d",age(5));
}
age(intn)
{intc;
if(n==1)c=10;
elsec=age(n-1)+2;
returnc;
}
结果:
18
【例三】在屏幕上显示杨辉三角形
1分析:
若起始行为第1行
1 1则:
第x行有x个值
121对第x行第y列,其值(不计左侧空格时)
13311(y=1或y=x)
146 41c(x,y)=
15101051c(x-1,y-1)+c(x-1,y)
………………
程序如下:
main()
{
inti,j,n;
clrscr();
printf("Inputn=");
scanf("%d",&n);
for(i=1;i<=n;i++)
{
for(j=0;j<=n-i;j++)
printf("");/*为了保持三角形态,此处输出两个空格*/
for(j=1;j<=i;j++)
printf("%3d",c(i,j));
printf("\n");
}
}
intc(intx,inty)
{
intz;
If(y==1||y==x)
return1;
else
{
z=c(x-1,y-1)+c(x-1,y);
returnz;
}
}
【例四】Fibonacci数列问题。
1(n=1)
分析:
fib(n)=1(n=2)
fib(n-1)+fib(n-2)(n>1)
程序如下:
fib(intn)
{
intf;
if(n==1||n==2)
f=1;
else
f=fib(n-1)+fib(n-2);
return(f);
}
main()
{
inti,s=0;
clrscr();
for(i=1;i<=12;i++)
s=s+fib(i);
printf("n=12,s=%d",s);
}
结果:
376
【例五】运行下列程序,当输入字符序列AB$CDE并回车时,程序的输出结果是什么?
#include
rev()
{
charc;
c=getchar();
if(c=='$')
printf("%c",c);
else
{
rev();
printf("%c",c);
}
}
main()
{
rev();
}
结果:
$BA
【例六】反向输出一个整数(非数值问题)
非数值问题的分析无法象数值问题那样能得出一个初值和递归函数式,但思路是相同的。
分析方法:
①简化问题:
设要输出的正整数只有一位,则“反向输出”问题可简化为输出一位整数。
②对大于10的正整数,逻辑上可分为两部分:
个位上的数字和个位以前的全部数字。
将个位以前的全部数字看成一个整体,则为了反向输出这个大于10的正整数,可按以下步骤:
a、输出个位上的数字;
b、将个位除外的其他数字作为一个新的整数,重复a步骤的操作。
其中b问题只是对原问题在规模上进行了缩小——递归。
所以,可将反向输出一个正整数的算法归纳为:
if(n为一位整数)
输出n;
else
{输出n的个位数字;
对剩余数字组成的新整数重复“反向输出”操作;
}
程序如下:
#include
voidmain()
{
voidprintn(intx);
intn;
printf("Inputn=");
scanf("%d",&n);
if(n<0)
{
n=-n;
putchar('-');
}
printn(n);
}
voidprintn(intx)/*反向输出整数x*/
{
if(x>=0&&x<=9)/*若x为一位整数*/
printf("%d",x);/*则输出整数x*/
else/*否则*/
{
printf("%d",x%10);/*输出x的个位数字*/
printn(x/10);/*将x中除个位数字外的全部数字形成新的x后,继续递归操作*/
}
}
执行:
Inputn=12345
结果:
54321
执行:
Inputn=-12479
结果:
-97421
【讨论】Inputn=123456
-6167
为什么:
123456710=111100010010000002
int类型的数实际只能存入16位,即11100010010000002
第1位为符号位,其余各位取反后再加1,得原码1001110111000000(真值-761610)
【例七】汉诺塔(TowerofHanoi)问题。
P118例7.9《试题汇编》【7.18】
也是一个非数值问题。
分析方法:
①简化问题:
设盘子只有一个,则本问题可简化为a→b。
②对于大于一个盘子的情况,逻辑上可分为两部分:
第n个盘子和除n以外的n-1个盘子。
如果将除n以外的n-1个盘子看成一个整体,则要解决本问题,可按以下步骤:
a、将a杆上n-1个盘子借助于b先移到c杆;a→c(n-1,a,c,b)
b、将a杆上第n个盘子从a移到b杆;a→b
c、将c杆上n-1个盘子借助a移到b杆。
c→b(n-1,c,b,a)
【注意】由于递归调用的堆栈占用不能控制,在RMX的进程中,不提倡递归调用。
六、变量的存储类型
模块化设计要求研究不同模块(函数、源文件)间变量的关系。
变量两大属性:
·数据类型·存储类别
1、数据类型(复习)
通过变量说明(定义)来规定其数据类型:
格式数据类型关键字变量名
如chara;
intb,c;
floatx,y;
为什么使用变量前要先对其数据类型进行“说明”——好比看电影前先买票订座
·预留存储空间(如char型为1个字节,int型为2个字节……)
·确定存储方式(如char型存放ASCII值,int型存放补码值……)
char型(用一个字节存放该字符的ASCII值)
int型(用两个字节存放该数值的补码)
float型(四个字节)
尾数(补码)阶码
一个变量的数据类型定义后,就规定了该变量只能存储相应类型的数据。
如定义intx,则x只能存放整型数,如果x=3.14159,会被自动转为整型存放。
2、存储类别P130
规定了变量在计算机内部的存放位置→决定变量的“寿命”(何时“生”,何时“灭”)
一个完整的变量说明格式如下:
存储类别数据类型变量名
如staticintx,y;
存储类别:
·register型(寄存器型)
变量值存放在运算器的寄存器中——存取速度快,一般只允许2~3个,且限于char型和int型,通常用于循环变量(在微机的TurboC中实际上自动转为auto型)
·auto型(自动变量型)
变量值存放在主存储器的动态存储区(堆栈方式)
优点——同一内存区可被不同变量反复使用
以上两种均属于“动态存储”性质,即调用函数时才为这些变量分配单元,函数调用结束其值自动消失。
·static型(静态变量型)
变量值存放在主存储器的静态存储区
程序执行开始至结束,始终占用该存储空间
·extern型(外部变量型)
同上,其值可供其他源文件使用
以上两种均属于“静态存储”性质,即从变量定义处开始,在整个程序执行期间其值都存在。
未说明存储类别时,函数内定义的变量为auto型,函数外定义的变量为extern型。
七、局部变量与全局变量
1、局部变量——函数内部或复合语句内定义的变量
auto(默认)所在函数调用结束时,其值自动消失
局部变量register
static所有函数调用结束,其值仍保留
如不赋初值,auto变量和register变量为随机值,static变量取初值为0(数值型)或空格(字符型)
所有形参都是局部变量
局部变量只在本函数或本复合语句内才能使用,在此之外不能使用(视为不存在)——main函数也不例外。
【例一】求程序运行结果
main()
{inta=2,i;
clrscr();
for(i=0;i<3;i++)
printf("%4d",f(a));
}
f(inta)
{
intb=0;
staticintc=3;
b++;
c++;
returna+b+c;
}
aibcf(a)
200→147
10→1 58
20→169
【结果】789
【例二】求程序运行结果
main()
{intk=4,m=1,p;
clrscr();
p=func(k,m);printf("%d,",p);
p=func(k,m);printf("%d",p);
}
func(inta,intb)
{staticintm=0,i=2;
i+=m+1;
m=i+a+b;
returnm;
}
k mabmi
41410→82→3
41418→173→12
【结果】8,17
2、全局变量——在函数之外定义的变量
extern(默认)允许本源文件中其他函数及其他源文件使用
全局变量static只限本源文件中使用
有效作用范围:
从定义变量位置开始直到本源文件结束
如果需要将全局变量的作用范围扩展至整个源文件——
法1:
全部在源文件开头处定义
法2:
在引用函数内,用extern说明
法3:
在源文件开头处,用extern说明
【例三】求程序运行结果
externintx,y;/*可以省略int。
如果没有此句,编译就会出错*/
{
clrscr();
printf("x=%d,y=%d\n",x,y);