ja6.docx
《ja6.docx》由会员分享,可在线阅读,更多相关《ja6.docx(60页珍藏版)》请在冰豆网上搜索。
ja6
第6章函数与编译预处理
结构化程序设计思想是采用自顶向下逐步细分的模块化设计原则,C语言中每个模块的功能是通过函数来实现的。
通过本章的学习,要求熟练掌握用户函数的结构、定义方法和调用方法;掌握函数调用中数据传递的几种方式;掌握函数调用的形式和规则、函数的返回值、函数的类型声明;了解嵌套和递归的概念及如何实现函数的嵌套调用和递归调用;掌握设计由多个函数组成的C程序的方法;了解变量存储类别的概念。
掌握编译预处理的语法形式和使用方法;掌握带参宏的定义和宏替换。
6.1概述
在解决一个规模较大的问题时,通常是先把它分成若干个小问题,然后集中精力去求解每个小问题。
在编写一个较大的程序时我们也是采用类似的处理办法,即把较大的程序分为若干个小程序模块,每个程序模块用来实现一个特定的功能。
在C语言中,每个程序模块的作用是由函数完成的。
一个C语言程序可由一个主函数和若干个函数构成。
由主函数调用其他函数,其他函数也可以调用别的函数。
同一个函数可以被一个或多个函数多次调用。
函数调用过程如图6.1所示。
main()sub1()
{…{…
sub1();}
…
sub2();sub2()
…{…
}}
图6.1
在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。
要善于利用函数库中的函数,以减少重复编写程序段的工作量。
先举一个简单的函数调用的例子。
【例6.1】
main()
{printstar();/*调用printstar函数*/
print_message();/*调用print_message*/
printstar();/*调用printstar函数*/
printstar()/*printstar函数*/
{
printf("**********************\n");
}
print_message()/*print_message函数*/
{
printf(“ThisisaCfunction!
\n”);
}
printstart()和print_message()都是用户定义的函数,分别用来输出一排“*”号和一行信息。
说明:
(1)一个源程序文件由一个或多个函数组成。
一个源程序文件是一编译单位,即以源程序为单位进行编译,而不以函数为单位进行编译。
(2)一个C程序由一个或多个源程序文件组成。
对较大的程序,一般不希望全放在一个文件中,而将函数和其他内容(如预处理)分别放在若干个源文件中,再由若干源文件组成一个C程序。
这样可以分别编写、分别编译,提高调度效率。
一个源文件可以为多个C程序公用。
(3)C程序的执行从main函数开始,调用其他函数后流程返回到main函数,在main函数中结束整个程序的运行,main函数是系统定义的。
(4)所有函数都是平行的,即在定义函数时是互相独立的,一个函数并不从属于另一个函数,即函数不能嵌套定义(这和其他的高级语言可能不同)。
函数间可以互相调用,但不能调用main函数。
(5)从用户使用的角度看,函数有两种;
①标准函数,即库函数。
这是由系统提供的,用户不必自己定义这些函数,可以直接使用它们。
应该说明,不同的C系统提供的库函数的数量和功能不同,当然有一些基本的函数是相同的。
②用户自己定义的函数,用户自己编写的用以解决特定问题。
(6)从函数的形式看,函数分两类;
①无参函数。
如例6.1中printstar()和print_message()就是无参函数。
在调用无参函数时,主调函数并不将数据传送给被调用函数,一般用来执行指定的一组操作(如例8.1那样),printstar()函数的作用是输出18个星号。
无参函数可以带回或不带回函数值。
②有参函数。
在调用函数时,在主调函数和被调用函数之间有数据传递。
也就是说,主调函数可以将数据传给被调用函数使用,被调用函数中的数据也可以带回来供主调函数使用。
6.2函数的定义
6.2.1无参函数的定义形式
类型标识符函数名()
{声明部分
语句
}
例6.1中的printstar()和print_message()都是无参函数。
用“类型标识符”指定函数值的类型,即函数带回来的值的类型。
若函数没有需要带回的数据,则类型标识符是void或者不写类型标识符。
6.2.2有参函数定义的一般形式
类型标识符函数据名(形式参数列表)
{声明部分
语句
}
例如:
intmax(intx,inty)
{intz;/*函数据体中的声明部分*/
z=x>y?
x:
y;
return(z);
}
如果在定义函数时不指定函数类型,系统会自动指定该函数类型为int型。
因此上面定义的max函数左端的int可以省写。
在老版本C语言中,对形参类型的声明是放在函数定义的笫2行,也就是不在笫1行的括号内指定形参的类型,而在括号外单独指定,例如上面定义的max函数可以写成以下形式:
intmax(x,y)/*指定形参x,y*/
intx,y;/*对形参指定类型*/
{intz;
z=x>y?
x:
y;
return(z);
}
一般把这种方法称为传统的对形参的声明方式,而把前面介绍过的方法称为现代的方式。
TurboC和目前使用的多数C版本对这两种方法都允许使用,两种用法等价,ANSI新标准推荐前一种方法,即现代方式。
6.2.3空函数
它的形式为:
类型说明符函数名()
{}
例如:
function(){}
调用此函数时,什么工作也不做,没有任何实际作用。
只是这些函数还未编好,先占一个位置,以后用一个编好的函数代替它。
这样做,程序的结构清楚、可读性好,以后扩充新功能方便,对程序结构影响不大。
6.2.4形式参数和实际参数
在调用有参函数时,主调函数要把一些数据传递给被调用函数进行处理,调用函数和被调用函数之间的数据传递是通过形式参数和实际参数实现的。
在函数定义中,函数名后面小括号中的变量称为“形式参数”(简称“形参”)。
在主调函数中调用一个函数时,函数名后面小括号中的数据(可以是常数或有具体值的变量,也可以是表达式)称为“实际参数”(简称“实参”)。
【例6.2】调用函数时的数据传递
main()
{inta,b,c;
scanf("%d,%d",&a,&b);
c=max(a,b);
printf("Maxis%d",c);
}
max(intx,inty)/*定义有参函数max*/
{
intz;
z=x>y?
x:
y;
return(z);
}
关于形参与实参的说明:
⑴在定义函数中指定的形参,在未出现函数调用时,它们并不占内存中的存储单元。
只有发生函数调用时,函数max中的形参才被分配内存单元。
在调用结束后,形参所占的内存单元也被释放。
⑵实参可以是常量、变量或表达式,如:
max(3,a+b);
但要求它们有确定的值,在调用时将实参的值赋给形参。
⑶在被定义的函数中,必须指定形参的类型(见例6.2程序第7行)。
⑷实参与形参的类型应相同或赋值兼容。
例6.2中实参和形参都整型,这是合法的、正确的。
如果实参为实型而形参为整型,则按第2章介绍的不同类型数据间转换规则先进行类型转换然后赋值。
假设实参的值为3.5,则将实数3.5转换成整数3,然后赋给形参。
⑸C语言规定,实参变量对形参变量的数据传递是“值传递”,即单向传递,只由实参传给形参,而不能由形参回传给实参。
在内存中,实参与形参占用不同的内存单元。
如图6.3所示。
在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参,调用结束后,形参单元被释放,实参单元仍保留并维持原值。
因此,在执行一次被调用函数时,形参的值如果发生改变,并不会改变主调函数中实参的值。
6.2.5函数的返回值
1)函数的返回值
很多时候我们都希望把被调用函数的处理结果返回给主调函数,即函数调用结束后能返回一个确定的值给主调函数,这就是函数的返回值(又叫函数值)。
例如,在例6.2中,max(2,3)的返回值是3,而max(5,2)的返回值是5。
主调函数中的语句“c=max(a,b);”将函数max的返回值赋给变量c。
下面对函数值作一些说明:
(1)函数的返回值是通过函数中的return语句获得的,return语句将被调用函数中的一个确定值带回主调函数中去。
(2)如果需要从被调用的函数带回函数据值(供主调函数使用),被调用函数中必须包含return语句,如果不需要从被调用函数值可以不要return语句。
(3)一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个语句起作用。
(4)return语句后面的括弧也可以不要,如
returnz;
它与“return(z);”等价。
(5)return后面可以是一个表达式,此时返回的是该表达式的值。
例如,例6.2中的函数max可以改写如下;
max(intx,inty)
{
return(x>y?
x:
y);
}
2)函数值的类型。
既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型,类型是int时可以省略。
例如:
intmax(floatx,floaty)(函数值为整型,这里的int可以省略)
charletter(charcl,charc2)(函数值为字符型)
doublemin(intx,inty)(函数值为双精度型)
在定义函数时,声明的函数值类型应该和return语句中表达式值的类型一致。
例如6.2中用隐含方式声明max函数返回值为整型,而变量z也被指定为整型,通过return语句把z的值作为max的函数值,由max带回主调函数。
z的类型与max函数值的类型是一致的,是正确的。
3)如果函数值的类型和return语句中表达式值的类型不一致,则以函数值的类型为准,自动进行类型转换。
即函数值的类型决定返回值的类型。
【例6.3】返回值类型与函数值类型不同。
将例6.2稍作改动(注意与例6.2的区别)。
main()
{
floata,b;
intc;
intmax(float,float);/*函数声明*/
scanf("%f,%f",&a,&b);
c=max(a,b);
printf("Maxis%d\n",c);
}
max(floatx,floaty)
{floatz;/*z为实型变量*/
z=x>y?
x:
y;
return(z);
}
运行结果如下:
1.5,2.5
maxis2
函数max的函数值为整型,而return语句中表达式z的值为实型,二者不一致,按上述规定,先将z转换为整型,然后将这个整型值返回给主调函数main。
如果将main函数中的c定义为实型,用%f格式符输出,也是输出2.000000。
有时,可以利用这一特点进行类型转换,如在函数中进行实型运算,希望返回的是整型量,可让系统去自动完成类型转换。
但这种做法往往使程序不清晰,可读性降低,容易弄错,而且也不是所有的类型都能互相转换(如实数与字符型数据之间),因此建议初学者不要采用这种方法,而应做到使函数据类型与return返回值的类型一致。
4)如果被调用函数中没有return语句,并不带回一个确定的、用户所希望得到的函数值,但实际上,函数并不是不带回值,而只是不带回有用的值,带回的是一个不确定的值。
例如,在例6.1的程序中,尽管没有要求函数printstar和print_message带回值,但是如果在程序中出现下面的语句也是合法的:
{inta,b,c;
a=printstar();
b=print_message();
c=printstar();
printf("%d,%d,%d\n",a,b,c);}
运行时除了得到和例6.1一样的结果外,还可以输出a、b、c的值(分别为23、22、23),a、b、c的值没什么实际意义(今printstar函数输出17个字符,返回值17;print_message输出16个字符,返回值为16)。
5)为了明确表示“不带回值”,可以用“void”定义“无类型”(或称“空类型)。
例如,例6.1中函数printstar和函数print_message的定义可以改为
voidprintstar()
{……}
voidprint_message()
{……}
这样,系统就保证不使函数带回任何值,即禁止在主调函数中使用被调用函数的返回值,如果已将printstar和print_message函数定义为void类型,则下面的用法就是错误的:
a=printstar();
b=print_message();
编译时会给出出错信息。
为使程序减少出错,保证正确调用,凡不要求带回函数值的函数,一般应定义为void类型。
6.3函数的调用
6.3.1函数调用的一般形式
有参函数调用的一般形式为:
函数名(实参表列);
无参函数调用的一般形式为:
函数名();
对于有参函数如果实参表列包含多个实参,则各参数间用逗号隔开。
实参与形参的个数应相等,类型应一致,实参与形参按顺序对应,一一传递数据。
如果是调用无参函数,则没有“实参表列”,但小括号不能省略,见例6.1。
需要说明的是,如果实参表列包括多个实参,对实参求值的顺序根据不同的C语言版本而定,有的系统按从左到右的顺序进行,而有的系统则按从右到左的顺序进行。
【例6.4】
main()
{inti=2,p;
p=f(i,++i);/*函数调用*/
printf("%d",p);
}
intf(inta,intb)/*函数定义*/
{intc;
if(a>b)c=1;
elseif(a==b)c=0;
elsec=-1;
return(c);
}
在TurboC系统下运行结果如下:
0
若按自左至右顺序求实参的值,则函数调用相当于f(2,3),程序运行结果应为“-1”。
若按自右至左顺序求实参的值,则函数调用相当于f(3,3),程序运行结果应为“0”,读者可以在所用的计算机系统上试一下,以便知道它所处理的方法。
由于存在上述情况,使程序通用性受到影响。
因此应当避免这种容易引起不同理解的情况。
如果本意是按自左而右顺序求实参的值。
可以改写为
j=i;
k=++i;
p=f(j,k);
如果本意是自右而左求实参的值,可改写为
j=++i;
p=f(j,j);
这种情况在printf函数中也同样的存在,如
printf(“%d,%d”,i,i++);
也发生上述同样的问题,若i的原值为3,在TurboC上运行结果为4,3。
请读者务必注意,应该避免这种容易混淆的用法。
6.3.2函数调用的方式
按函数在程序中出现的位置来分,可以有以下三种函数调用方式:
1.函数语句
把函数调用作为一个语句,如例6.1中的
printstar();
这时不要求函数带回值,只要求函数完成一定的操作。
2.函数表达式
函数出现在一个表达式中,这种表达式称为函数表达式。
这时要求函数带回一个确定的值以参加表达式运算。
例如:
c=2*max(a,b);
函数max是表达式的一部分,它的值乘2再赋给c。
3.函数参数
函数调用作为一个函数的实参。
例如:
m=max(a,max(b,c));
其中max(b,c)是一次函数调用,它的值作为max另一次调用的实参。
m的值是a、b、c三者中最大的。
又如:
printf("%d",max(a,b));
也是把max(a,b)作为printf函数的一个参数。
6.3.3对被调用函数的声明和函数原型
在一个函数中调用另一个函数(即被调用函数)需要具备哪些条件呢?
(1)首先被调用的函数必须是已经存在的函数(是库函数或用户自已定义的函数)。
但光有这一条件还不够。
(2)如果调用库函数,一般还应该在本程序开头用#include命令将调用有关库函数时所需用到的信息“包括”到本程序中来。
例如,前几章中已经用过的
#include〈stdio.h〉
其中“stdio.h”是一个“头文件”。
在stdio.h文件中包含了输入输出库函数所用到的一些宏定义信息。
如果不包括“stdio.h”文件中的信息,就无法使用输入输出库中的函数。
同样,使用数学库中的函数,应该用
#include〈math.h〉
.h是头文件的后缀。
(3)如果使用用户自己定义的函数,而且该函数与调用它的函数(即主调函数)在同一个文件中,一般还应该在主调函数中对被调用的函数作声明,即向编译系统声明将要调用此函数,并将有关信息通知编译系统。
【例6.5】对被调用的函数作声明。
main()
{floatadd(floatx,floaty);/*对被调用函数的声明*/
floata,b,c;
scanf("%f,%f",&a,&b);
c=add(a,b);
printf("sumis%f",c);
}
floatadd(floatx,floaty)/*函数首部*/
{floatz;/*函数体*/
z=x+y;
return(z);
}
运行结果如下:
3.6,6.5↙
sumis10.100000
注意程序第2行:
floatadd(floatx,floaty);是对被调用的add函数作声明。
注意:
对函数的“定义”和“声明”不是一回事。
“定义”是指对函数功能的确立,包括指定函数名、函数值类型、形参及其类型、函数体等,它是一个完整的、独立的函数单位。
而“声明”的作用则是把函数的名字、函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如函数是否正确,实参与形参的类型和个数是否一致)。
从程序中可以看到对函数的声明与函数定义中的第1行(函数首部)基本上是相同的。
因此可以简单地照写已定义的函数的首部,再加一个分号,就成为了对函数的“声明”。
其实,在函数声明中也可也不写形参名,而只写形参的类型。
如:
floatadd(float,float):
在C语言中,以上的函数声明称为函数原型(functionprototype)。
使用函数原型是ANSIC的一个重要特点。
它的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。
从例6.5中可以看到main函数的位置在add函数的前面,而在进行编译时是从上到下逐渐进行的。
如果没有对函数声明,当编译到包含函数调用的语句“c=add(a,b);”时,编译系统不知道add是不是函数名,也无法判断实参(a和b)的类型和个数是否正确,因而无法进行正确性的检查。
只有在运行时才会发现实参与形参的类型或个数不一致,出现运行错误。
但是在运行阶段发现错误并重新调试程序,是比较麻烦的,工作量也较大。
应当在编译阶段尽可能多地发现错误,随之纠正错误。
现在我们在函数调用之前用函数原型做了函数声明。
因此编译系统记下了所需调用的函数的有关信息,在对“c=add(a,b);”进行编译时就“有章可循”了。
编译系统根据函数的原型对函数调用的合法性进行全面的检查。
和函数原型不匹配的函数调用会导致编译出错,它属于语法错误,用户根据屏幕显示的出错信息很容易发现和纠正错误。
函数原型的一般形式为
(1)函数类型函数名(参数类型1,参数类型2,……);
(2)函数类型函数名(参数类型1参数名1,参数类型2参数名2,……);
第
(1)种形式是基本形式。
为了便于阅读程序,也允许在函数原型中加上参数名,就成了第
(2)种形式。
但编译系统不检查参数名。
因此参数名是什么无所谓,例6.5上面程序中的声明也可以写成
floatadd(floata,floatb);/*参数名不用x、y,而用a、b*/
效果完全相同。
应当保证函数原型与函数首部写法上的一致,即函数类型、函数名、参数个数、参数类型和参数顺序必须相同。
函数调用时函数名、实参个数应与函数原型一致。
实参类型必须与函数原型中的形参类型赋值兼容,按第2章介绍的赋值规则进行类型转换。
如果不是赋值兼容,就按出错出处理。
几点说明:
(1)如果在函数调用之前,没有对函数作声明,则编译系统则会把第一次遇到的该函数形式(函数定义或函数调用)作为函数的声明,并将函数类型默认为int型。
如例6.2在调用max函数之前没有进行函数声明,编译时首先遇到的函数形式是函数调用“max(a,b)”,由于以原型的处理是不考虑参数名的,因此系统将max()加上int作为函数声明,即
intmax();
因此,如果函数类型为整型,可以在函数调用前不作声明。
但是使用这种方法时,系统无法对参数的类型做检查。
若调用函数时参数使用不当,在编译时也不会报错。
因此,为了程序清晰和安全,建议都加以声明为好。
如在例6.2中最好加上以下函数声明;
intmax(int,int);
或
intmax(intx,inty);
(2)如果被调用函数的定义出现在主调函数之前,可以不必加以声明。
因为编译系统已经知道了已定义的函数类型,会根据函数首部提供的信息对函数的调用作正确性检查。
如果把例6.5改写如下(即把main函数放在add函数的后面),就不必在main函数中对add函数进行声明。
(3)如果已在所有函数定义之前且在函数外部作了声明,则在各个主调函数中不必对所调用的函数作声明。
例如:
charletter(char,char);/*以下3行在所有函数之前,且在函数外部*/
floatf(float,float);
inti(float,float);
main()
{……}/*不必声明它所调用的函数*/
chatletter(charcl,charc2)/*定义letter函数*/
{……}
floatf(floatx,floaty)/*定义f函数*/
{……}
inti(floatj,floatk)/*定义i函数*/
{……}
除了以上所提到的三种情况外,都应该要对所调用的函数作声明,否则编译时就会出错。
用函数原型来声明函数,还能减少编写程序时可能出现的错误,由于函数声明的位置与函数调用语句的位置比较近,因此在写程序时便于就近参照函数原型来书写函数调用,不易出错。
6.4函数的嵌套和递归调用
6.4.1函数的嵌套调用
C语言的函数定义是互相平行、独立的,也就是说在定义一个函数时不能包含另一个函数的定义,即不能嵌套定义函数。
但可以在调用一个函数的过程中,又调用另一个函数,即函数允许嵌套调用。
见图6.5。
main函数a函数b函数
①②
调用a函数③④
⑨⑧调用b函数⑤
结束⑦⑥
图6.5
图6.5表示的是3层嵌套(连main函数在内),其很执行过程是:
⑴执行main函数的开头部分;
⑵执行到“调用a函数”处,流程转到a函数;
⑶执行a函数的开头部分;
⑷执行到“调用b函数”处,流程转到b函数;
⑸执行b函数的全部操作;
⑹b函数执行完后自动返回到a函数中“调用b函数”处;
⑺继续执行a函数中尚未执行的部分;
⑻a函数执行完后自动返回到main函数中“调用a函数”处;
⑼继续执行main函数的剩余部分直到结束。
求Cmn=m!
/(n!
*(m-n)!
)
从程序可以看到:
(1)在定义函数时,函数名为f、xpoint、root的3个函