6
第六章函 数
2009年 7月 27日星期一2
第六章 函数要编好程序,就要合理地划分程序中的各个程序块,C语言中称这种程序块为函数。
本章介绍函数的定义、调用方法及与函数应用相关的问题。
2009年 7月 27日星期一3
函数 (Function)是按给定的任务,把相关语句组织在一起的程序块,也称为例程或过程。
函数分为标准库函数和用户自定义函数。
标准库函数,可以在任何程序中使用的公共函数,由 C
编译器提供。
用户自定义函数,必须按 C要求通过函数定义、函数说明,才能被调用,由用户自定义。
程序从 main( )开始,一个函数可以调用其他函数,也可以被其他函数调用。
函数与函数之间通过传递参数和返回值相联系 。
C不允许函数嵌套定义 。 在一个函数中再定义一个函数是非法的 。
6.1函数概述
2009年 7月 27日星期一4
6.2.1 函数定义定义的一般形式,
返回值类型定义符 函数名 (形式参数表 )
{
函数体
}
其中:
函数名 —给自定义的函数取的名字。
形式参数表 —函数被调用时,接收调用函数传递的实参数。
函数体 —执行功能的语句块。
类型定义符 —通过函数调用获得的函数返回值,通常用
return语句返回。类型定义符是指函数返回值的数据类型。
例如:
int area_s(int a,int b)
{
return(a*b)
}
6.2函数定义和调用
2009年 7月 27日星期一5
1.无参函数的一般形式类型说明符 函数名 ()
{
类型说明语句
}
例 6.1:定义一个无参函数 (6_1.cpp)
void Hello()
{
printf("Hello,how do you do,\n");
}
2009年 7月 27日星期一6
2有参函数的一般形式类型说明符 函数名 (形式参数表 )
形式参数类型说明
{
类型说明语句
}
例 6.2,函数,求两个数中较小的数 (6_2.cpp)
int min(int a,int b)
{
if(a>b) return b;
else return a;
}
2009年 7月 27日星期一7
6.2.2 函数调用
( 1)函数调用的一般格式:
函数名 (实参数表 );
( 2)调用实质:
程序执行流程转向由函数名指定的被调用函数。
实参数一一对应地传递给函数定义中的形参数。
执行函数定义中的函数体。
执行结束,通过 return语句将值返回到调用处。
程序执行流程返回调用处。执行后面的语句。
2009年 7月 27日星期一8
( 3)函数调用的几种情况函数调用语句
printstar( );
函数调用返回值构成表达式
C=2*area_s(l,w);
函数调用返回值作为函数实参数
m=max(a,max(b,c));
2009年 7月 27日星期一9
例如:
main( )
{
int i,v;
scanf(”%d”,&i);
v=cube(i); /* 函数调用返回值赋给变量 v */
printf(”cube=%d\n”,v);
}
int cube(int x) /* 自定义函数 */
{
int cb;
cb=x*x*x;
return(cb); /* 函数返回值 */
}
输入,5 输出,cube=125
2009年 7月 27日星期一10
如:
int area_s(int a,int
b);
也可简写为:
int area_s(int,int);
注意,当前很多编译程序对所有函数,
无论什么情况,都要求写函数原型,
以帮助函数调用时的错误查找 。 因此对程序所使用的所有函数都写函数原型,是一种好的编程风格 。
6.2.3 函数原型函数原型 (Function Prototyping)是一条程序语句,必须以分号结束。它由函数返回类型、函数名和参数表构成,其形式与函数定义的头部相似。
格式,返回类型 函数名 (参数表 );
对于用户自己定义的函数,只要函数调用点在自定义函数之前,程序员必须在源代码中说明函数原型。函数原型可不包含形参数变量名,只包含形参数类型名。
2009年 7月 27日星期一11
例如,编写一个求两个电阻的串联值和并联值的程序。
main( )
{
float series(float,float); float parallel(float,float);
float r1,r2,rt; char sp;
printf(”Enter resistor r1,r2:”);
scanf(”%f%f”,&r1,&r2);
printf(”Enter?s?,?p?:”);
while((sp=getchar( ))==?\n?) /* 去掉 scanf语句输数后的换行符
*/ ;
if(sp==?s?)
rt=series(r1,r2); /*函数调用 */
else if(sp==?p?)
rt=parallel(r1,r2); /*函数调用 */
printf(“The total resistance=%f ohms\n”,rt);
}
/*函数原型说明 */
2009年 7月 27日星期一12
float series(rs1,rs2) /* 自定义函数 */
float rs1,rs2;
{
float rs;
rs=rs1+rs2;
return(rs);
}
float parallel(rp1,rp2) /*自定义函数 */
float rp1,rp2;
{
float rp;
rp=(rp1*rp2)/(rp1+rp2);
return(rp);
}
2009年 7月 27日星期一13
函数调用的参数传递方式有,
6.3 函数参数传值 ——单向传递 即 实参 形参传址 ——双向传递 即 实参 形参传数值形参不能改变实参传地址形参能改变实参
6.3.1 传值调用
C语言通常使用传值调用的方法传递参数,前面的例子均使用这种方法。
在传值调用中,只是实参的复制值被传递给形参,实参与形参不再有任何其他联系。因此,形参不能改变实参。
2009年 7月 27日星期一14
6.3.2 传址调用传值是一种单向传递,形参不能改变实参。在程序设计中有时需要实现双向传递,使形参的值能直接改变实参的值。为了实现这个目的,通常使用传地址调用。
传地址调用的方法:
1、传递实参的地址;
2、形参必须定义为指针变量;
3、函数调用时,实参地址 → 形参指针。形参指向的目标变量即为实参数,形参的改变将影响实参的改变。
2009年 7月 27日星期一15
例如:下面程序通过传址调用实现两个整型数的交换
#include <stdio.h>
void swap(int *,int *); /*函数原型说明 */
main()
{
int a=3,b=8;
printf(” a=%d,b=%d\n”,a,b);
swap(&a,&b); /* 传址调用 */
printf(”After swapping… \n”);
printf(” a=%d,b=%d\n”,a,b);
}
void swap(int *x,int *y) /*函数定义,形参定义为指针 */
{
int temp=*x;
*x=*y;
*y=temp;
}
运行结果:
a=3,b=8
After swapping…
a=8,b=3
2009年 7月 27日星期一16
传址调用的内存分配情况如下图所示:
栈 区
temp
y
x
3
0067:F090
0067:F092
……
8
3
……
b
a
*y
*x
0067:F090
0067:F092
swap( int *x,int *y)
swap( &a,&b)
main ( )
2009年 7月 27日星期一17
如将上例改为传值调用,则两个整型变量的值不能交换。
#include <stdio.h>
void swap(int,int); /*函数原型说明 */
main()
{
int a=3,b=8;
printf(” a=%d,b=%d\n”,a,b);
swap(a,b); /* 传值调用 */
printf(”After swapping… \n”);
printf(” a=%d,b=%d\n”,a,b);
}
void swap(int x,int y) /*函数定义 */
{
int temp=x;
x=y;
y=temp;
}
运行结果:
a=3,b=8
After swapping…
a=3,b=8
2009年 7月 27日星期一18
6.4 函数与数组数组名即为定义的数组起始地址。
当数组作为实参进行函数调用时,由传址方式实现。
将此实参对应的形参定义为指针或类型、大小相同的另一数组。由指针实现对原数组数据项的访问。
因此,从指针角度看,当用数组名作为实参数调用函数时,指向该数组第一个元素的指针就被传递到函数中,
然后,函数可以利用指针来访问数组的各个元素。
例如:下面是数组作为实参进行函数调用的例子。
2009年 7月 27日星期一19
#include <stdio.h>
void sum(int *array,int n){
int sum=0;
for(int i=0;i<n;i++){
sum+=*array;array++;
}
printf(”%d\n”,sum);}
main( ){
int a[10]={1,2,3,4,5,6,7,8,9,10};
sum(a,10);} 运行结果,55
例:求数组各项的累加和。
2009年 7月 27日星期一20
用数组名作为函数参数时还应注意以下几点:
1,形参数组和实参数组的类型必须一致,否则将引起错误。
2,形参数组和实参数组的长度可以不相同,因为在调用时,只传送首地址而不检查形参数组的长度。当形参数组的长度与实参数组不一致时,
虽不至于出现语法错误 (编译能通过 ),但程序执行结果将与实际不符,
这是应予以注意的。
3,在函数形参表中,允许不给出形参数组的长度,或用一个变量来表示数组元素的个数。
4,多维数组也可以作为函数的参数。在函数定义时对形参数组可以指定每一维的长度,也可省去第一维的长度。
2009年 7月 27日星期一21
6.5 函数与指针本节讨论有关函数与指针的两个问题,返回指针的函数和指向函数的指针。
6.5.1 返回指针的函数(指针型函数)
返回值为指针的函数称为指针型函数。可以返回全局或静态变量的地址,但不能返回局部变量的地址。
一般形式,
类型定义符 *指针型函数名 ( )
{ 函数体 }
“类型定义符,是指返回指针指向的数据类型。
例,程序在输入一个字符串中查找一个给定的字符,如果找到,
则从该字符开始打印余下的子字符串,及该字符是字符串的第几个字符;否则输出,no match found”。
2009年 7月 27日星期一22
#include <stdio.h>
int count;
main( )
{
char s[80],ch,*p,*match();
gets(s);
ch=getchar( );
p=match(ch,s); /* 函数调用,返回地址赋 p指针 */
if(p)
printf(”%s%d\n”,p,(count+1));
else
printf(”no match found”);
}
2009年 7月 27日星期一23
char *match(c,sp) /* 定义指针型函数 */
char c,*sp;
{ count=0;
while(c!=sp[count]&&sp[count]!=?\0?)
count++;
if(sp[count])
return(&sp[count]); /* 返回子字符串的地址 */
return (0);
}
运行结果:
输入,programming
a
输出,amming 6
2009年 7月 27日星期一24
6.5.2 指向函数的指针 (函数指针 )
在 C中,与数组类似函数名也表示在内存区域的首地址。
即,函数调用的入口地址。
同样,使用指向函数起始地址的指针变量,也可以把函数作为实参数传递给被调函数。
指向函数的指针简称为函数指针。
定义的一般形式:
类型定义符 (*指针变量名 )( );
例如,int (*p)( );
调用格式,(*指针变量名 )(实参表 )
其中,,实参表,为作为实参的函数所需要的实参数。
2009年 7月 27日星期一25
例,int (*pa)( ); /* 定义 pa为指向函数的指针 */
int add(int,int); /* 函数 add( )的原型说明 */
pa=add; /*函数指针 pa指向函数 add( ) */
int z=(*pa)(x,y); /*(*pa)(x,y)表示使用指针 pa调用函数 add( )*/
注意,x,y是使用函数指针 pa调用函数 add( )时传递给 add( )
的实参数。
当函数作为实参进行函数调用时,其实参数使用函数名,形参应定义为指向函数的指针。
2009年 7月 27日星期一26
6.6函数与结构由于结构可以整体赋值,所以可以将结构作为值参数传递给函数,也可以定义返回结构值的函数。这样,要函数处理存储在结构中的数据,我们至少有三种不同方法:
1,个别地将结构成员的值传递给函数处理。
2,将整个结构作为参数值传递给函数,一般将这种参数称作结构参数。
3,将结构的地址传给函数,也就是说传递指向结构的指针值。这称为结构指针参数。
2009年 7月 27日星期一27
6.6.1 结构指针及结构变量的传址调用在 ANSI C标准中允许用结构变量作函数参数进行整体传送。但是这种传送要将全部成员逐个传送,特别是成员为数组时将会使传送的时间和空间开销很大,
严重地降低了程序的效率。
因此最好的办法就是使用指针,即用指针变量作函数参数进行传送。这时由实参传向形参的只是地址,
从而减少了时间和空间的开销。
2009年 7月 27日星期一28
例 6.17:计算一组学生的平均成绩和不及格人数。 (6_17.cpp)
#include<stdio.h>
struct stu{ int num; char *name;char sex;
float score;}boy[5]={ {101,"Li ping",'M',45},
{102,"Zhang ping",'M',62.5},{103,"He fang",'F',92.5},
{104,"Cheng ling",'F',87},{105,"Wang ming",'M',58},};
void main(){ struct stu *ps;
void ave(struct stu *ps);
ps=boy;
ave(ps);}
void ave(struct stu *ps){ int c=0,i;float ave,s=0;
for(i=0;i<5;i++,ps++)
{ s+=ps->score;
if(ps->score<60) c+=1; }
printf("s=%f\n",s);
ave=s/5;
printf("average=%f\nNo pass student count=%d\n",ave,c);}
2009年 7月 27日星期一29
6.6.2 结构型函数结构型函数指处理结构型参数的函数。
例 6.18:求平面两点之间的殴氏距离。
(6_18.cpp)
2009年 7月 27日星期一30
递归函数 (Recursive Function)即自调用函数,即在函数体内有直接或间接地自己调用自己的语句。
例如,求 n的阶乘
n! = n(n-1)! (当 n>1时 )
n! = 1 (当 n=0,1时 )
下面是求 n!的函数
long fact(int n)
{
if (n = =1)
return 1;
else
return (n*fact(n-1));
}
6,7 递归函数
2009年 7月 27日星期一31
如,n 等于 3,则递归调用过程如下;
fact(3) = 3 * fact( 2 )
12 * 1
2 * fact( 1)
1 * fact( 0 )
11 * 1
23 * 2
6
由于自调用过程在函数内必须设置某些条件,当条件成立时终止自调用过程,并使程序控制逐步从函数中返回。
递归调用机制是栈数据结构实现的。
函数之间由参数传递和返回值联系。
2009年 7月 27日星期一32
图 6- 2 递归调用示意图
2009年 7月 27日星期一33
大多数递归函数都能用非递归函数代替。
例如:求两个整数 a,b 的最大公约数递归,long gcd1(int a,int b){
if (a%b = = 0)return b;
return gcd1(b,a%b);}
非递归,long gcd2(int a,int b){
int temp;while (b!=0)
{ temp=a%b;
a=b;b=temp;
}return a;
}
使用递归函数的目的是简化程序设计,提高程序的可读性,但增加系统开销。
2009年 7月 27日星期一34
例 6.21,Hanoi塔问题 (6_21.cpp)
如图 6- 3(a)所示 (n=3)。一块板上有三根针,A,B,
C。 A针上套有 n(n=3)个大小不等的圆盘,大的在下,小的在上。要把这 n个圆盘从 A针移动 C针上,移动规则是,(1) 一次只能移动一个; (2) 大的不能放在小的上面; (3) 只能在三个位置中移动。求移动的步骤
2009年 7月 27日星期一35
在 C中程序是在操作系统环境下运行的,因此 main( )
函数和其他函数一样,也可以带有参数。
从操作系统传递信息到 main( )中最常用的方法是使用命令行参数。
使用命令行参数,可以使一个可执行程序的执行过程,像使用操作系统命令一样,在命令行中提供运行参数。
6.8命令行参数
2009年 7月 27日星期一36
使用命令行参数的形式为:
void main(int argc,char *argv[ ])
{ ……
}
其中的两个特殊的内部形参数:
argc 命令行参数个数,比实际参数个数多 1;
*argv[ ] 字符型指针数组,每个元素分别为指向命令行参数
(字 符串常量)的指针。
例如,argv[0] 指向程序本身;
argv[1] 指向命令行参数第一个字符串常数;
argv[2] 指向命令行参数第二个字符串常数;
……
argc和 argv是习惯常用的参数名,也可另取名字。
2009年 7月 27日星期一37
例如:下面程序说明了命令行参数的使用方法,其功能是打印命令行参数。
/* cla.c */
#include <stdio.h>
main(int argc,char *argv[ ])
{
int icount=0;
while(icount<argc)
{
printf(“arg%d;%s\n”,icount,argv[icount]);
icount++;
}
}
2009年 7月 27日星期一38
运行过程:就经编译、连接,生成可执行文件 cla.exe。
C>cla.exe data element statement <回车 >
运行结果:
arg0,cla
arg1,data
arg2,element
arg3,statement
2009年 7月 27日星期一39
例:下面是使用命令行参数求两个非负整数的最大公约数的程序。
/* gcd.cpp */
#include <stdio.h>
#inclued <stdlib.h>
int gcd(int,int);
main(argc,argv)
int argc;
char *argv[];
{
int a,b,result;
if(argc!=3)
{
printf(”Enter two integer data:”);
exit(0);
}
2009年 7月 27日星期一40
result=gcd(atoi(argv[1]),atoi(argv[2]));
printf(“%d\n”,result);
}
int gcd(u,v)
int u,v;
{
int temp;
while(v!=0)
{
temp=u%v;
u=v;
v=temp;
}
return(u);
}
运行过程:
C>gcd.exe 15 9 <回车 >
结果:
3
2009年 7月 27日星期一41
6.9标准库函数标准库函数是指由 C编译器开发者为用户预先编写的具有特定功能的一系列函数。以程序库的方式提供使用。
包括:数学函数、输入输出函数、存储分配函数、进程控制函数、字符串操作函数、系统调用函数、时间处理函数、
图形处理函数等常用的函数。
,头部文件,包含有用于类型检查的各种标准库函数的原型定义、宏定义和常量定义等。并按功能分类。如:
math.h,string.h,dos.h,graphics.h ……,
因此,使用标准库函数时,在程序中必须使用 include 预处理命令,包含标准库函数所在的头文件。
C语言允许将自定义的函数,放入自己建立的程序库中。
2009年 7月 27日星期一42
例 6.24:输入五个国家的名称按字母顺序排列输出 (6_24.cpp)
本题编程思路如下:
五个国家名应由一个二维字符数组来处理。然而 C语言规定可以把一个二维数组当成多个一维数组处理。因此本题又可以按五个一维数组处理,而每一个一维数组就是一个国家名字符串。用字符串比较函数比较各一维数组的大小,
并排序,输出结果即可
2009年 7月 27日星期一43
第六章小结
1.函数是 C语言程序中最重要的结构,它是支持程序设计中的模块和层次结构的基础。
2.C语言中的函数定义就是编写完成某种功能的程序模块 。
函数中包括三个部分:函数头,参数说明和函数体 。 函数头中的数据类型定义符定义了函数返回值的数据类型;函数名是为函数定义的名字,它是调用函数的标识符;形式参数表中的形参变量用于接收调用函数传递的实参变量 。 参数说明是对形参表中形参变量类型的说明,调用函数时实参数必须与形参数的数据类型相同,其顺序必须一一对应,函数体是完成某种功能的语句集合 。
3.函数调用是将实参数传递给被调函数形参,执行函数体的过程 。 函数返回语句结束函数的执行并将控制返回到调用处 。 函数的返回值能在表达式中作为操作数 。
2009年 7月 27日星期一44
4.数据在函数间传递可采用四种方式,即传值,传址,利用返回值,利用外部变量 。 传值方式是采用复制方式把实参的值传递给形参,它们各自占用独立的存储空间,形参的任何改变不影响实参 。 传址方式是将实参的地址传递给形参,为此,接收地址的形参必须被说明成指针变量,通过指针形参的任何改变实质上就是对指针指向的实参的改变 。 利用函数 return语句可以返回函数处理数据的结果 。 利用各函数都可以对外部变量进行访问的特点,可以在函数间实现数据的传递 。 使用外部变量的方式将在下章中介绍 。
5.函数与数组就是当数组作为实参传递时,利用不带下标的数组名将数组的首地址传递给函数,被调函数用指针变量接收这个地址之后,利用指针与数组元素建立的对应关系,可以对数组进行处理 。 实质上这是传址方式调用的扩展,函数中的传址方式调用是指针应用的一个重要方面 。
2009年 7月 27日星期一45
6,指针型函数与一般函数定义方式不同之处仅在于,为了表示函数的返回值不是数值而是指向数值的指针,必须在函数名前加一个,*” 号,程序中接收指针型函数返回值的变量必须说明为指向同种数据类型的指针变量 。
7.指向函数的指针简称为函数指针,定义函数指针与一般指针的形式不同,应注意其区别 。 其定义形式是:
类型定义符 (*函数指针变量 )();
函数指针的作用是在函数调用时传递实参函数,它是通过将实参函数的起始地址传递到函数指针实现的 。
8.递归函数就是函数直接或间接地调用自己 。 递归总是有条件的,无终止条件的递归是无意义的 。 递归函数是采用堆栈机制实现的,递归函数源程序代码紧凑,但它并不能节省内存空间和提高速度 。