【C/C++基础】06_函数
1. 概述
在前面已经介绍过,C源程序是由函数组成的。虽然在前面各章的程序中大都只有一个主函数main(),但实用程序往往由多个函数组成。函数是C源程序的基本模块,通过对函数模块的调用实现特定的功能。C语言中的函数相当于其它高级语言的子程序。C语言不仅提供了极为丰富的库函数(如Turbo C,MS C都提供了三百多个库函数),还允许用户建立自己定义的函数。用户可把自己的算法编成一个个相对独立的函数模块,然后用调用的方法来使用函数。可以说C程序的全部工作都是由各式各样的函数完成的,所以也把C语言称为函数式语言。
由于采用了函数模块式的结构,C语言易于实现结构化程序设计。使程序的层次结构清晰,便于程序的编写、阅读、调试。
在C语言中可从不同的角度对函数分类。
1.1 从函数定义角度分类
从函数定义的角度看,函数可分为库函数和用户定义函数两种。
1 库函数:由C系统提供,用户无须定义,也不必在程序中作类型说明,只需在程序前包含有该函数原型的头文件即可在程序中直接调用。在前面各章的例题中反复用到printf、scanf、getchar、putchar、gets、puts、strcat等函数均属此类。
2 用户定义函数:由用户按需要写的函数。对于用户自定义函数,不仅要在程序中定义函数本身,而且在主调函数模块中还必须对该被调函数进行类型说明,然后才能使用。
C语言提供了极为丰富的库函数,这些库函数又可从功能角度作以下分类。
1) 字符类型分类函数:用于对字符按ASCII码分类:字母,数字,控制字符,分隔符,大小写字母等。
2) 转换函数:用于字符或字符串的转换;在字符量和各类数字量(整型,实型等)之间进行转换;在大、小写之间进行转换。
3) 目录路径函数:用于文件目录和路径操作。
4) 诊断函数:用于内部错误检测。
5) 图形函数:用于屏幕管理和各种图形功能。
6) 输入输出函数:用于完成输入输出功能。
7) 接口函数:用于与DOS,BIOS和硬件的接口。
8) 字符串函数:用于字符串操作和处理。
9) 内存管理函数:用于内存管理。
10) 数学函数:用于数学函数计算。
11) 日期和时间函数:用于日期,时间转换操作。
12) 进程控制函数:用于进程管理和控制。
13) 其它函数:用于其它各种功能。
1.2 从函数返回值角度分类
C语言的函数兼有其它语言中的函数和过程两种功能,从这个角度看,又可把函数分为有返回值函数和无返回值函数两种。
1 有返回值函数:此类函数被调用执行完后将向调用者返回一个执行结果,称为函数返回值。如数学函数即属于此类函数。由用户定义的这种要返回函数值的函数,必须在函数定义和函数说明中明确返回值的类型。
2 无返回值函数:此类函数用于完成某项特定的处理任务,执行完成后不向调用者返回函数值。这类函数类似于其它语言的过程。由于函数无须返回值,用户在定义此类函数时可指定它的返回为“空类型”,空类型的说明符为“void”。
1.3 从参数传递角度分类
从主调函数和被调函数之间数据传送的角度看又可分为无参函数和有参函数两种。
1) 无参函数:函数定义、函数说明及函数调用中均不带参数。主调函数和被调函数之间不进行参数传送。此类函数通常用来完成一组指定的功能,可以返回或不返回函数值。
2) 有参函数:也称为带参函数。在函数定义及函数说明时都有参数,称为形式参数(简称为形参)。在函数调用时也必须给出参数,称为实际参数(简称为实参)。进行函数调用时,主调函数将把实参的值传送给形参,供被调函数使用。
1.4 从函数能否被其他源文件调用的角度分类
函数本质上是全局的,因为一个函数要被另外的函数调用,但是也可以制定函数只能被本文件调用,而不能被其他文件调用。根据函数能否被其他源文件调用,将函数区分为内部函数和外部函数。
1. 内部函数
定义:一个函数只能被本文件中其他函数调用,它称为内部函数。
格式:
static 类型标识符 函数名(形参表)
2. 外部函数
定义:在函数首部的最左边冠以关键字extern,则表示此安徽省农户是外部函数,可以供其他文件调用。
格式:
extern 类型标识符 函数名(形参表)
注释:
(1)C++允许在声明函数时省略extern.
(2)#include指令的应用:利用函数原型扩展函数作用域。
还应该指出的是,在C语言中,所有的函数定义,包括主函数main在内,都是平行的。也就是说,在一个函数的函数体内,不能再定义另一个函数,即不能嵌套定义。但是函数之间允许相互调用,也允许嵌套调用。习惯上把调用者称为主调函数。函数还可以自己调用自己,称为递归调用。
main 函数是主函数,它可以调用其它函数,而不允许被其它函数调用。因此,C程序的执行总是从main函数开始,完成对其它函数的调用后再返回到main函数,最后由main函数结束整个程序。一个C源程序必须有,也只能有一个主函数main。
2. 函数定义
2.1 无参函数的定义形式
类型标识符 函数名()
{声明部分
语句
}
其中类型标识符和函数名称为函数头。类型标识符指明了本函数的类型,函数的类型实际上是函数返回值的类型。该类型标识符与前面介绍的各种说明符相同。函数名是由用户定义的标识符,函数名后有一个空括号,其中无参数,但括号不可少。
{}中的内容称为函数体。在函数体中声明部分,是对函数体内部所用到的变量的类型说明。在很多情况下都不要求无参函数有返回值,此时函数类型符可以写为void。
2.2 有参函数定义的一般形式
类型标识符 函数名(形式参数表列)
{声明部分
语句
}
有参函数比无参函数多了一个内容,即形式参数表列。在形参表中给出的参数称为形式参数,它们可以是各种类型的变量,各参数之间用逗号间隔。在进行函数调用时,主调函数将赋予这些形式参数实际的值。形参既然是变量,必须在形参表中给出形参的类型说明。
2.3 函数的形式参数和实际参数
形参出现在函数定义中,在整个函数体内都可以使用,离开该函数则不能使用。实参出现在主调函数中,进入被调函数后,实参变量也不能使用。形参和实参的功能是作数据传送。发生函数调用时,主调函数把实参的值传送给被调函数的形参从而实现主调函数向被调函数的数据传送。
函数的形参和实参具有以下特点:
1. 形参变量只有在被调用时才分配内存单元,在调用结束时,即刻释放所分配的内存单元。因此,形参只有在函数内部有效。函数调用结束返回主调函数后则不能再使用该形参变量。
2. 实参可以是常量、变量、表达式、函数等,无论实参是何种类型的量,在进行函数调用时,它们都必须具有确定的值,以便把这些值传送给形参。因此应预先用赋值,输入等办法使实参获得确定值。
3. 实参和形参在数量上,类型上,顺序上应严格一致,否则会发生类型不匹配”的错误。
4. 函数调用中发生的数据传送是单向的。即只能把实参的值传送给形参,而不能把形参的值反向地传送给实参。因此在函数调用过程中,形参的值发生改变,而实参中的值不会变化。
2.4 函数的返回值
函数的值是指函数被调用之后,执行函数体中的程序段所取得的并返回给主调函数的值。对函数的值(或称函数返回值)有以下一些说明:
1) 函数的值只能通过return语句返回主调函数。
return 语句的一般形式为:
return 表达式;
或者为:
return (表达式);
该语句的功能是计算表达式的值,并返回给主调函数。在函数中允许有多个return语句,但每次调用只能有一个return 语句被执行,因此只能返回一个函数值。
2) 函数值的类型和函数定义中函数的类型应保持一致。如果两者不一致,则以函数类型为准,自动进行类型转换。
3) 如函数值为整型,在函数定义时可以省去类型说明。
4) 不返回函数值的函数,可以明确定义为“空类型”,类型说明符为“void”。如:
void s(int n)
{ ……
}
一旦函数被定义为空类型后,就不能在主调函数中使用被调函数的函数值了。例如,在定义s为空类型后,在主函数中写下述语句
sum=s(n);就是错误的。为了使程序有良好的可读性并减少出错, 凡不要求返回值的函数都应定义为空类型。
3. 函数的调用
3.1 函数调用的一般形式
C/C++语言中,函数调用的一般形式为:
函数名(实际参数表)
对无参函数调用时则无实际参数表。实际参数表中的参数可以是常数,变量或其它构造类型数据及表达式。各实参之间用逗号分隔。
3.2 函数调用的方式
在C/C++语言中,可以用以下几种方式调用函数:
1. 函数表达式:函数作为表达式中的一项出现在表达式中,以函数返回值参与表达式的运算。这种方式要求函数是有返回值的。例如:z=max(x,y)是一个赋值表达式,把max的返回值赋予变量z。
2. 函数语句:函数调用的一般形式加上分号即构成函数语句。例如: printf("%d",a);scanf ("%d",&b);都是以函数语句的方式调用函数。
3. 函数实参:函数作为另一个函数调用的实际参数出现。这种情况是把该函数的返回值作为实参进行传送,因此要求该函数必须是有返回值的。例如: printf("%d",max(x,y)); 即是把max调用的返回值又作为printf函数的实参来使用的。在函数调用中还应该注意的一个问题是求值顺序的问题。所谓求值顺序是指对实参表中各量是自左至右使用呢,还是自右至左使用。对此,各系统的规定不一定相同。介绍printf 函数时已提到过,这里从函数调用的角度再强调一下。
【例6.1】
main()
{
int i=8;
printf("%d\n%d\n%d\n%d\n",++i,--i,i++,i--);
}
如按照从右至左的顺序求值。运行结果应为:
8
7
7
8
如对printf语句中的++i,--i,i++,i--从左至右求值,结果应为:
9
8
8
9
应特别注意的是,无论是从左至右求值, 还是自右至左求值,其输出顺序都是不变的,即输出顺序总是和实参表中实参的顺序相同。由于Turbo C现定是自右至左求值,所以结果为8,7,7,8。
3.3 对被调用函数的声明和函数原型
函数声明:在函数尚未定义的情况下,事先将该函数的有关信息通知编译系统,以便使得编译能正常运行。
在主调函数中调用某函数之前应对该被调函数进行说明(声明),这与使用变量之前要先进行变量说明是一样的。在主调函数中对被调函数作说明的目的是使编译系统知道被调函数返回值的类型,以便在主调函数中按此种类型对返回值作相应的处理。
函数原型的一般形式为:
类型说明符 被调函数名(类型 形参,类型 形参…);
或为:
类型说明符 被调函数名(类型,类型…);
括号内给出了形参的类型和形参名,或只给出形参类型。这便于编译系统进行检错,以防止可能出现的错误。
函数原型的主要作用:根据函数原型在程序编译阶段对调用函数的合法性进行全面检查。
C语言中又规定在以下几种情况时可以省去主调函数中对被调函数的函数说明。
1) 当被调函数的函数定义出现在主调函数之前时,在主调函数中也可以不对被调函数再作说明而直接调用。
2) 函数声明的位置可以在调用函数所在的函数中,也可以在函数外。如在所有函数定义之前,在函数外预先说明了各个函数的类型,则在以后的各主调函数中,可不再对被调函数作说明。例如:
char str(int a);
float f(float b);
main()
{
……
}
char str(int a)
{
……
}
float f(float b)
{
……
}
其中第一,二行对str函数和f函数预先作了说明。因此在以后各函数中无须对str和f函数再作说明就可直接调用。
3) 对库函数的调用不需要再作说明,但必须把该函数的头文件用include命令包含在源文件前部。
3.4 函数的嵌套调用
C语言中不允许作嵌套的函数定义。因此各函数之间是平行的,不存在上一级函数和下一级函数的问题。但是C语言允许在一个函数的定义中出现对另一个函数的调用。这样就出现了函数的嵌套调用。即在被调函数中又调用其它函数。这与其它语言的子程序嵌套的情形是类似的。其关系可表示如图。
图表示了两层嵌套的情形。其执行过程是:执行main函数中调用a函数的语句时,即转去执行a函数,在a函数中调用b 函数时,又转去执行b函数,b函数执行完毕返回a函数的断点继续执行,a函数执行完毕返回main函数的断点继续执行。
3.5 函数的递归调用
一个函数在它的函数体内调用它自身称为递归调用。这种函数称为递归函数。C语言允许函数的递归调用。在递归调用中,主调函数又是被调函数。执行递归函数将反复调用其自身,每调用一次就进入新的一层。
例如有函数f如下:
int f(int x)
{
int y;
z=f(y);
return z;
}
这个函数是一个递归函数。但是运行该函数将无休止地调用其自身,这当然是不正确的。为了防止递归调用无终止地进行,必须在函数内有终止递归调用的手段。常用的办法是加条件判断,满足某种条件后就不再作递归调用,然后逐层返回。下面举例说明递归调用的执行过程。
【例6.2】用递归法计算n!
用递归法计算n!可用下述公式表示:
n!=1 (n=0,1)
n×(n-1)! (n>1)
按公式可编程如下:
long ff(int n)
{
long f;
if(n<0) printf("n<0,input error");
else if(n==0||n==1) f=1;
else f=ff(n-1)*n;
return(f);
}
main()
{
int n;
long y;
printf("\ninput a inteager number:\n");
scanf("%d",&n);
y=ff(n);
printf("%d!=%ld",n,y);
}
【例6.3】Hanoi塔问题
一块板上有三根针,A,B,C。A针上套有64个大小不等的圆盘,大的在下,小的在上。要把这64个圆盘从A针移动C针上,每次只能移动一个圆盘,移动可以借助B针进行。但在任何时候,任何针上的圆盘都必须保持大盘在下,小盘在上。求移动的步骤。
本题算法分析如下,设A上有n个盘子。
如果n=1,则将圆盘从A直接移动到C。
如果n=2,则:
1.将A上的n-1(等于1)个圆盘移到B上;
2.再将A上的一个圆盘移到C上;
3.最后将B上的n-1(等于1)个圆盘移到C上。
如果n=3,则:
A. 将A上的n-1(等于2,令其为n`)个圆盘移到B(借助于C),步骤如下:
(1)将A上的n`-1(等于1)个圆盘移到C上。
(2)将A上的一个圆盘移到B。
(3)将C上的n`-1(等于1)个圆盘移到B。
B. 将A上的一个圆盘移到C。
C. 将B上的n-1(等于2,令其为n`)个圆盘移到C(借助A),步骤如下:
(1)将B上的n`-1(等于1)个圆盘移到A。
(2)将B上的一个盘子移到C。
(3)将A上的n`-1(等于1)个圆盘移到C。
到此,完成了三个圆盘的移动过程。
从上面分析可以看出,当n大于等于2时,移动的过程可分解为三个步骤:
第一步 把A上的n-1个圆盘移到B上;
第二步 把A上的一个圆盘移到C上;
第三步 把B上的n-1个圆盘移到C上;其中第一步和第三步是类同的。
当n=3时,第一步和第三步又分解为类同的三步,即把n`-1个圆盘从一个针移到另一个针上,这里的n`=n-1。 显然这是一个递归过程,据此算法可编程如下:
move(int n,int x,int y,int z)
{
if(n==1)
printf("%c-->%c\n",x,z);
else
{
move(n-1,x,z,y);
printf("%c-->%c\n",x,z);
move(n-1,y,x,z);
}
}
main()
{
int h;
printf("\ninput number:\n");
scanf("%d",&h);
printf("the step to moving %2d diskes:\n",h);
move(h,'a','b','c');
}
4. C++的函数特性
4.1 内联函数
程序通过一组函数实现是一种好的设计方法。但是函数调用涉及执行时间的开销。C++提供的内联函数可以减少函数调用的开销。内联函数的定义格式:
inline <函数值类型> <函数名>(<形式参数表>)
{
函数体
}
(1)对用户来说,内联函数的定义与调用与普通函数的使用方法是相似的。
(2)作为编译系统,它将程序中调用内联函数的语句(或表达式)用内联函数体中的代码进行替换。这样在执行时就避免了对内联函数的调用,从而减少了因函数调用所增加的时间开销,提高了程序运行的效率。
(3)使用内联函数可以节省运行时间,但却增加了目标程序的的长度。因此一般只将规模很小而使用频繁的简单函数声明为内联函数。
4.2 函数重载
定义:对一个函数名重新赋予它新的含义,使得一个函数名可以多用。所谓重载,就是一物多用。函数可以重载,运算符也可以重载。
重载函数的参数个数、参数类型和参数顺序中至少有一种不同,函数返回值类型可以相同也可以不同。
4.3 函数模板
模板又叫泛型编程,分为函数模板与类模板两部分。
函数模板不是一个实在的函数,编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述,当它具体执行时,将根据传递的实际参数决定其功能。
(1)函数模板的含义:建立一个一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型代替,这个通用函数就称为函数模板。
(2)函数模板定义的一般形式:
或者:
类型参数可以不止一个,可以根据 需要确定个数。如:
template <class T1, typename T2>
参数类型T,T1.T2是一个虚拟的类型名,表示模板中出现的是一个类型名。
(3) 函数模板只适合于函数体相同、函数参数个数相同而类型不同的情况。如果参数个数不同,不能使用函数模板。
4.4 有默认参数的函数
(1)含义:给形式参数一个默认的值,形参不一定必须从实参取值;
(2)可以给任意多个形参以默认值。实参与形参的结合顺序是从左往右,含有默认值的形参必须在形参列表中的最右端,否则会出错。
如:void f1(float a, int c, int b=0, char d='a');
(3)如果函数的定义在函数的调用之前,则应在函数定义中给出默认值。为了避免混淆,最好只在函数声明时制定默认值。
(4)一个函数不能即作为重载函数,又作为有默认参数的函数。否则编译器因无法判定调用哪一个函数而报错。