函数
1 概述
一个较大的程序一般应分为若干个程序模块,第一个模块用来实现一个特定的功能,所有的高级语言中都有子程序这个概念,用子程序实现模块的功能,在C语言中,子程序的作用是由函数完成的,一个C程序可由一个主函数和若干个函数构成,由主函数调用其它函数,其它函数也可以互相调用,用一个函数可以被一个或多个函数调用任意多次。
在程序设计中,常将一些常用的功能模块编写成函数,放在函数库中供公共选用。要善于利用函数,以减少重复编写程序段的工作量。
从用户使用的角度来看函数有两种
(1)标准函数,即库函数,这是由系统提供的,用户不必自己定义这些函数,可以直接使用。
(2)用户自定义函数。
从函数的形式开看,函数分两类
(1)无参函数,在调用无参函数时,主调函数并不将数据传送给被调用函数。
(2)有参函数,在调用函数时,在主调函数和被调用函数之间有数据传递。
2 函数定义的一般形式
(1)无参函数定义形式
类型标识符 函数名()
{
声明部分
语句
}
用 类型标识符指定函数值的类型,即函数带回来的值的类型,无参函数一般不需要带回函数值,因些可以不写类型标识符。
(2)有参函数定义的一般形式
类型标识符 函数名(形式参数列表)
{
声明部分
语句
}
如果在定义函数时不指定函数类型,系统会隐含指定函数类型为int型
(3)空函数
它的形式为:
类型说明符 函数名()
{}
调用此函数时,什么不做,没有任何实际作用,用于以后扩充函数功能时补充上。
3 函数参数和函数的值
3.1 形式参数和实际参数
在调用函数时,大多数情况下,主调函数和被调用函数之间有数据传递关系。这就是前面提到的有参函数,前面已提到,在定义函数时函数名后面的括弧中的变量名称为形式参数简称形参,在主调函数中调用一个函数时,函数名后面括弧中的参数(可以是一个表达式)称为实际参数简称实参。
关于形参与实参的说明
(1)在定义函数中指定的形参,在未出现函数调用时,它们并不占内在中的存储单元,只有在发生函数调用时,函数形参才被分配内在单元,在调用结束后,形参所占的内在单元也被释放。
(2)实参可以是常量,变量也可以是表达式。但要求它们有确定的值,在调用时钭实参的值赋给形参
(3)在被定义的函数中,必须指定形参的类型。
(4)实参与形参的类型应相同或赋值兼容。
(5)C语言规定,实参变量对形参变量的数据传递是 值传递,即单向传递,只是由实参传给形参,而不能由形参传回来给实参,在内在中,实参单元与形参单元是不同的单元。
在调用函数时,给形参分配存储单元,并将实参对应的值传递给形参,调用结束后,形参单元被释放,实参单元仍保留并维持原值,因此在执行一个被调用函数时,形参的值如果发生改变,并不会改变主调函数的实参值。
3.2 函数的返回值
通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数的返回值。
(1)函数的返回值是通过函数中的return语句获得的。return语句将被调用函数中的一个确定值带回主调函数中去。
如果需要从被调用函数带回一个函数值,被调用函数中必须包含return语句,如果不需要从被调用函数带回函数值,可以不要return语句。
一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个语句起作用。
如:
return z;
return (z);
return (x>y?x:y);
(2)函数值的类型,既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型。
如:
int max(float x, float y);//返回整型
char letter(char c1, char c2);//返回字符型
double min(int x, int y);//返回双精度型
(3)如果函数值的类型和retrun语句中表达式的值不一致,则以函数类型为准,对数值型数据,可以自动进行类型转移,即函数类型决定返回值的类型。
(4)如果被调用函数中没有return语句,并不带回一个确定的,用户所希望得到的函数值,但实际上,函数江不是不带回值,而只是不带回有用的值,带回的是一个不确定的值。
(5)为了明确表示 不带回值,可以用 void 定义 无类型 或称 空类型
如:
void prinstar();
4 函数的调用
4.1 函数调用的一般形式
函数名(实参列表);
如果是调用无参函数,则 实参列表可以没有,但括弧不能省略。如果实参列表包含多个实参,则各参数间用逗号隔开,实参与形参的个数应相等,类型一致。实参与形参按顺序对应,一一传递数据,但如果实参列表包括多个实参,对实参求值的求值顺序不是确定的,有的系统按自左至右,有的系统则按自右至左求实参的值。
如Turbo C 和 MS c是按自右而左的顺序求值
#include <stdio.h>
void main()
{
int i = 1,p;
p = f(i, ++i);
printf("%d", p);
}
int f(int a, int b)
{
int c;
if(a > b)
c = 1;
else if(a == b)
c = 0;
else
c = -1;
return c;
}
在 Trubo c 上运行的结果是 0
如果按自左而右的顺序求实参的值,则函数调用相当于f(2, 3)
如果按自右而左的顺序求实参的值,则函数调用相当于f(3, 3)
4.2 函数调用的方式
(1)函数语句
把函数调用作为一个语句如:
printstar(); 这时不要求函数带回值,只要函数完成一定的操作。
(2)函数表达式
函数出现在一个表达式中,这种表达式称为函数表达式,这时要求函数带回一个确定的值以参加表达式的运算。如:
c = 2 * max(a, b);
(3)函数参数
函数调用作为一个函数的实参如:
m = max(a, max(b, c);
函数调用作为函数的参数,实质上也是函数表达式调用的一种,因为函数的参数本来就是要求是表达式形式。
4.3 对被调用函数的声明和函数原型
(1)被调用的函数必须是已经存在的函数。
(2)如果使用库函数,一般还应该在本文件开头用 #include 命令将调用有关库函数时所需用到的信息 包含 到本文件中来如:
#include <stdio.h>
(3)如果使用用户自定义函数,而且函数与调用它函数(即主调函数)在同一个文件中,一般还应该在主调函数中对被调用的函数作声明,即向编译系统声明要调用此函数,并将有关信息通知编译系统。如:
#include <stdio.h>
float add(float x, float y);//函数声明
void main()
{
float a, b, c;
scanf("%f%f", &a, &b);
c = add(a, b);
printf("%d", c);
}
float add(float x, float y)
{
float z;
z = x + y;
return z;
}
对函数的定义与声明不是一回事,定义是指对函数功能的确立,包括指定函数名函数值类型,形参及其类型,函数体等,它是一个完整的独立的函数单位,而声明的作用则是把函数的名字,函数类型以及形参的类型,个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查,因此可以简单地照写已定义的函数的首部,再加一个分号,就成为了对函数的声明。
其实,在函数声明中也可不写形参名而只写形参类型。如:
float add(float, float);
在C语言中以上的函数声明称为函数原型,使用函数原型是ANSI C 的一个重要特点,它的作用主要是利用它在程序的编译阶段对调用函数的合法性进行全面检查。
5 函数嵌套调用
C语言不能嵌套定义函数,但可以嵌套调用函数也就是说,在调用一个函数的过程中,又调用另一个函数。
6 函数递归调用
在调用一个函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。C语言的特点之一就是在于允许函数的递归调用。
如:
int f(int x)
{
int y, z;
z = f(y);
return (2 * z);
}
从上面的可以看到,这种递归调用都是无终止的自身调用,程序中不应该出现这种无终止的递归调用,而只应出现有限次数的,有终止的递归调用,这可以用if语句来控制,只有在某一条件成立时才继续执行递归调用,否则就不再继续。
用递归实现汉诺塔问题。
塔内有3个座A,B,C 开始时,A座上有64个盘子,盘子大小不等,大的在下小的在上,有一个老和尚想把这64个盘子从A座移到C座,但每次只允许移动一个盘子,且在移动过程中在3个座上都始终保持大盘在上小盘在下,在移动过程中可以利用B座。
分析:
(1)命令第2个和尚将63个盘子从A座移到B座。
(2)自已将1个盘子(最底下的,最大的盘子)从A座移到C座。
(3)再命令第2个和尚将63个盘子从B座移到C座。
但这有一个问题,第2个和尚怎么才能将63个盘子从A座移到B座,为了解决将63个盘子从A座移到B座,第2个和尚又想,如果有人能将62个盘子从一个座移到另一个座,我就能将63个盘子从A座移到B座,他是这样做的:
(1)命令第3个和尚将62个盘子从A座移到C座。
(2)自己将1个盘子从A座移到B座。
(3)再命令第3个和尚将62个盘子从C座移到B座
再进行一次递归,如此层层下放直到后来找到第63个和尚,让他完成将2个盘子从一个座移到另一座,进行到此,问题就接近解决了,最后找到第64个和尚,让他完成将1个盘子从一个座移到另一个座,至此全部工作都已落实,都是可以执行的。
可以看出,递归的结束条件是最后一个和尚只需移一个盘子,否则递归还要继续进行下去。
应当说明,只有第64个和尚的任务园成后,第63个和尚的任务才能完成,只有第2到第64个和尚任务完成,第1个和尚的任务才能完成,这就是一个典型的递归问题。
为使问题简化,我们先分析将A座上3个盘子移到C座上的过程。
(1)将A座上2个盘子移到B座上(借助C)
(2)将A座上1个盘子移到C座上
(3)将B座上2个盘子移到C座上(借助A)
其中第2步可以直接实现,第1步又可以用递归方法分解为
(2.1)将A上1个盘子多A移到C
(2.2)将A上1个盘子从A移到B
(2.3)将C上1个盘子从C移到B
第3步可以分解为:
(3.1)将B上1个盘子从B移到A上
(3.2)将B上1个盘子从B移到C上
(3.3)将A上1个盘子从A移到C上
将以上综合起来可以得到移动3个盘子的步骤为
A->C, A->B, C->B, A->C, B->A, B->C, A->C. `
共经历7步,由此可以推出,移动n个盘子经历2^n-1步。
由上分析可知,将n个盘子从A座移到C座,可以分解为以下3个步骤
(1)将A上n-1个盘子借助C座先移到B座上
(2)把A座上剩下的一个盘子移到C座上
(3)将n-1个盘子从B座借助A座移到C座上
上面的第1步与第3步都是把n-1个盘子从一个座移到另一个座上,采取的方法是一样的,只是座的名称不一样。
因此可以把上面的3个步骤分成两类操作:
(1)将n-1个盘子从一个座移到另一个座上(c>1),这就是大和尚让小和尚做的工作,它是一个递归的过程,即和尚将任务层层下放,直到第64个和尚为止。
(2)将1个盘子从一个座上移到另一座上,这是大和尚自己做的工作。
程序如下:
#include <stdio.h>
/*
函数move说明
移到盘子所用,参数 c1 被移动的座,c2 移动到的座
*/
void move(char c1, char c2)
{
printf("%c - > %c\n", c1, c2);
}
/*
函数hanoi说明
参数说明
int n 移动的盘子的个数
char one 被移动的座
char two 借助移动的座
char three 移动到的座
*/
void hanoi(int n, char one, char two, char three)
{
if(n == 1)
move(one, three);//递归退出判断,如果只有最后一个盘子就移动A座上的一个盘子到C座
else
{
/*
移动分开3步
(1)将n-1个盘子从A(one)移动到B(two)借助C(three)
(2)移动最后一个盘子从A(one)到C(three)
(3)将n-1个盘子从B(two)座上移动到C(three)上借助A(one)
*/
hanoi(n - 1, one, three, two);
move(one, three);
hanon(n - 1, two, one, three);
}
}
void main()
{
int m;
m = 3;
hanoi(m, 'A', 'B', 'C');//函数调用,有m个盘子,从A座移动到C座,借助B座
}