函数是C程序设计的核心,也是C程序设计的哲学。
C语言的库函数是由编译器提供的完整函数。
本次将介绍用户自定义函数
顾名思义,这是由你——程序员定义的函数。
本次将介绍以下内容:
●什么是函数,函数由哪几部分组成
●用函数进行结构化程序设计的优点
●如何在函数中声明局部变量
●如何从函数将值返回程序
●如何传递参数给函数
一.理解函数:
要理解函数,首先要弄懂什么是函数和如何使用函数。
1.1函数定义
函数是已命名的、执行专项任务的独立C代码段,可选择是否向调用它的程序返回一个值。
现在,仔细分析这段定义。
函数是已命名的。每个函数都有独一无二的函数名。
在程序的其他部分使用函数名,可以执行该函数中的语句。
这也称为调用(call)函数。可以在函数中调用其他函数。
函数是独立的。函数可独立执行任务,无需程序其他部分干页。
函数可以向调用它的程序返回一个值。
程序调用函数时,会执行函数中的语句。
如果需要的话,可以把特定信息传递给调用它们的程序。
以上便是定义函数需要了解的内容。往下学习之前,请先记住这些内容。
1.2函数示例:
输入:
//该程序演示一个简单的函数
#include <stdio.h>
long cube(long x);
long input, answer;
int main(void)
{
printf("Enter an integer value: ");
scanf("%ld", &input);
answer = cube(input);
//注意:%d是long类型整数的转换说明
printf("\nThe cube of %ld is %d.\n", input, answer);
return 0;
}
// 函数:cube() - 计算一个变量的立方值
long cube(long x)
{
long x_cubed;
x_cubed = x * x* x;
return x_cubed;
}
输出1:
输出2:
输出3:
解析:
第4行是函数原型,即位于程序后面的函数的模型。
函数的原型包括函数名、传递给该函数的变量类型和参数列表,以及返回的变量类型(如果有返回值的话)。
查看第4行可知函数名为cube,接受一个long类型的变量,并返回一个long类型的变量。
被传递给该函数的变量称为参数,位于函数名后面的圆括号中。
在该例中,cube函数只有一个参数: long x。函数名前面的关键字表明其返回值的类型。
本例中,该函数返回一个long类型的变量。
第12行调用函数cube,并将变量input作为参数传递给该函数。
第6行将input变量和answer变量都声明为1ong类型。这与第4行的函数原型所使用的类型相匹配。
函数本身称为函数定义。
在该例中,被调用的函数是cube,其函数定义在第21~27行。
与函数原型类似,函数定义也由几个部分组成。
函数开始于第21行的函数头。函数头是函数的开始,给出函数的名称(本例中,函数名为cube )、返回类型,以及描述函数接受的参数。
注意,函数头与函数原型完全相同,只是函数头末尾没有分号。
花括号括起来的是函数体(第22~27行)。
调用函数时,将执行函数体中的语句(如第25行)。
第23行是变量声明,看上去和以前见过的变量声明一样,但是稍有不同,这是局部变量声明。
在函数体中声明的变量是局部变量。
最后,第26行是return语句,表明函数结束。
该例中,return语句将一个值(x_cubed变量)传递给调用它的程序。
main()也是一个函数,比较cube() 函数和main()函数,会发现它们的结构相同。
还使用其他函数,如printf() 和scanf(),虽然它们都是库函数,但是和用户自定义的函数一样,也是有参数和返回值的函数。
二.函数的工作原理:
只有在C程序的其他部分调用函数时才会执行函数中的语句。
程序在调用函数时,以传递一个或多个参数的形式给函数传递信息。
实参是程序发送给函数的数据。
函数可以使用这些数据执行函数中的语句,完成之前设计好的任务。
执行完函数中的语句后,程序将跳转至原来调用该函数时的位置继续执行。
函数以返回值的形式将信息传回程序。
下列图所示的程序中包含3个函数,每个函数都被调用一次。
程序每次调用函数时,都会跳转执行函数中的内容。
执行完函数后,再跳转回程序调用该函数时的位置。
可根据需要,以任意顺序调用函数多次。
现在,已经知道什么是函数以及函数的重要性,接下来将介绍如何创建并使用自己的函数。
2.1语法:
1.函数原型
返回类型函数名(参数类型参数名1,... 参数类型参数名n)
2.函数定义
返回类型函数名(参数类型参数名1,... 参数类型参数名n)
{
/* 语句 */
}
函数原型为编译器描述了程序后面即将定义的函数。函数原型包括返回类型(表明该函数将返回的变量类型)、函数名(描述函数的用途)和函数接受的参数类型。参数名为可选,也就是说,函数原型中的参数名可写可不写。函数原型以分号结尾。
函数定义是实际的函数。函数定义包括需要执行的代码。如果函数原型包含变量名,函数定义的第1行(称为函数头)应该与函数原型相同。另外,虽然函数原型中的参数名可选,但是在函数头中必须包含参数名。紧跟函数头的是函数体,包含函数要执行的语句。函数体以左花括号开始,右花括号结束。如果函数的返回类型不是void,函数体中就必须包含return语句,返回一个与返回类型匹配的值。即使函数的返回类型是void,也可以在函数中包含没有返回值的return语句。在所有的函数中都包含return语句,是非常好的习惯.
函数原型示例:
double squard( double number );
void print_report( int report_number );
int get_menu_choice( void );
函数定义示例:
double squard( double number ) //函数头
{ //左花括号
return ( number * number ) //函数体
} //右花括号
void print_report( int report_number )
{
if (report_number == 1 )
puts( "Printing Report 1" );
else
puts("Not printing Report 1");
return;
}
三.函数和结构化程序设计:
在C程序中使用函数,可以练习结构化程序设计——由独立的代码段单独执行程序任务。
“ 独立的代码段”看起来很像之前的函数定义,的确,函数和结构化程序设计密切相关。
3.1 结构化程序设计的优点:
结构化设计之所以卓越,有以下两个重要原因。
1.编写结构化程序更容易。将复杂的问题分解成若干简单的小任务,每个任务都由一个函数来完成,各函数中的代码和变量独立于程序的其他部分。每次都处理较简单的任务,程序可以执行的更快。
2.调试结构化程序更容易。如果程序出现bug (有时会导致程序无法正常运行),结构化设计能让你轻松地单独处理特定代码段的问题。
结构化程序设计还有一个优点:可以通过复用代码段来节约时间。
如果你在程序中编写了一个执行某项任务的函数,便可在另一个执行相同任务的程序中复用它。
即使新程序中要完成的任务稍有不同,但是修改一个已有的函数比重新写一个新函数容易得多。
回想一下,你用过多少次printf()和scanf() 函数(虽然你可能不知道它们具体的代码)。
如果创建了一个执行单独任务的函数,更方便用于其他程序中。
3.2规划结构化程序:
如果要编写结构化程序,首先要进行规划。
你可以在动手写代码之前做这件事,只需要纸和笔。
程序的计划应该是程序的特定任务列表。
如果你打算写一个管理通讯录(姓名和地址的列表)的程序,希望程序做什么?显然,该程序应该能完成下面的事项:
●输入新的姓名和地址;
●修改现有的条目;
●按姓氏排列条目;
●打印邮件标签;
你已经将程序分成4个主要的任务,每个任务都要用一个函数来完成。
现在,进一步分析,将这些任务再细分成子任务。
例如,“输入新的姓名和地址”任务可细分为以下子任务:
●从磁盘中读取现有地址列表;
●提示用户输入新的条目;
●在列表中添加新的日期;
●将已更新的列表保存至磁盘中;
同样,“修改现有条目” 任务也可细分为以下子任务:
●从磁盘中读取现有地址列表;
●修改一条或多条条目;
●将已更新的列表保存至磁盘中;
也许你也注意到上述任务中有两个共同的子任务一从磁盘读取和保存列表。
因此,可以编写“从磁盘中读取现有地址列表”函数和“将已更新的列表保存至磁盘中”函数,这两个函数都可以被“输入新的姓名和地址”函数和“修改现有条目”函数调用。
把程序分成若干子任务后,很容易发现其中包含的共同部分。
另外,如果编写的磁盘访问函数有两种用途(读取和保存),程序会更小、效率更高。
用这种方法编写的程序具有层次化的结构。
以上面分析的通讯录程序为例,如图所示:
按照本次介绍的方法规划时,很快就能把程序需要完成的多个独立任务联系起来。
然后,一次处理一个任务,把注意力集中在相对简单的任务上。写完一个函数并能正常运行后,再继续完成下一个任务。
在不知在不觉中,你的程序就慢慢成型了。
3.3 自上而下的方法:
C程序员采用自上而下的方法进行结构化程序设计。如上图所示,程序的结构类似一棵倒过来的树。许多情况下,程序的大部分工作实际上都是由“树枝末端”的函数来完成,而靠近“树干”的函数主要是直接引导程序执行这些函数。
因此,许多C程序的主体(main()函数)中只有少量代码引导程序执行函数,而程序的大部分代码都在函数中。
通常,程序会给用户提供一份菜单,然后程序将按照用户的选择执行不同的函数。
注意:使用菜单是一种不错的程序设计方法。后面会介绍如何使用switch语句创建通用菜单驱动系统。
现在,你知道了什么是函数,明白了函数的重要性。接下来的将介绍如何创建自己的函数。
在动手写代码前要先规划。提前确定程序的结构,可以节约写代码和调试程序的时间。
不要在一个函数中完成所有的任务,一个函数应该只完成一项任务。
四.编写函数:
确定要函数做什么,是编写函数的第一步。要明确这一点:
4.1函数头:
每个函数的第1行都是函数头。
函数头由3部分组成,分别有不同的功能,如图所示。
(1)返回类型
函数的返回类型指定了该函数返回调用程序的数据类型。
函数的返回类型可以是C语言的任意数据类型,包括char、int、float、或double等。
当然,函数也可以没有返回值,这种情况下函数的返回类型为void。
下面有一些示例:
int func1
(...) //返回int类型
float func2
(...) //返回float类型
void func3
(...) //无返回值
如上所示:
func1 返回一个整型数
func2 返回一个浮点型数
func3无返回值。
(2)函数名
函数名的命名规则遵循C语言变量的命名规则。
在C程序中,函数名必须唯一(不能与其他函数和变量同名)。函数名应反映该函数的功能或用途。
(3) 形参列表
大多数函数都使用实参(argument)。
实参是在调用函数时,传递给函数的值。函数要知道每个待传入实参的数据类型,函数头的形参列表便提供了实参类型的信息。
可以给函数传递C语言的任意数据类型。
形参列表必须为每个传递给函数的实参提供一个相应的项(由形参类型和形参名组成)。
例如,下面是函数头:
long cube(long x)
该形参列表中的long x,指定了该函数需要一个long类型的实参,由形参x表示。
如果形参列表中有多个形参,要用逗号隔开它们。
如下面的函数头,指定了func1有3个参数:一个int类型的x .一个float类型的y和一个char类型的z :
void func1
(int x, float y, char z)
有些函数没有参数,应在形参列表中写上void,如:
void func2
(void)
不要在函数头末尾加分号,否则,编译器会生成错误信息。
其中形参和实参在这里很容易混淆。
形参是函数头中的项,可视为实参的“占位符”。函数的形参是固定的,在程序执行期间不可更改。
实参是调用函数的程序传递给函数的实际值。每次调用函数,都可以传递不同的实参。
在C语言中,每次调用函数时,传递给函数的实参类型和数量必须相同,但实参的值可以不同。
在函数中,通过使用相应的形参名来访问实参。
下面用一个示例来讲解上述内容。程序中是一个简单的程序,将调用一个函数3次。
输入:
//解释实参和形参的区别
#include <stdio.h>
float x = 3.5, y = 65.11, z;
float half_of(float k);
int main(void)
{
// 在本次调用中,x是half_of()的实参。
z = half_of(x);
printf("The value of z = %f\n", z);
//在本次调用中,y是half_of()的实参。
z = half_of(y);
printf("The value of z = %f\n", z);
//在本次调用中,z是half_of()的实参。
z = half_of(z);
printf("The value of z = %f\n", z);
return 0;
}
float half_of(float k)
{
// k是形参。每次调用half_of()时,
//k中的值是传递给half_of()的实参的值
return (k / 2);
}
输出:
下列图演示了形参与实参之间的关系:
解析:
在程序中,第7行声明了half_of() 的函数原型。
第12、16和20行调用half_of() ,
第26行和第32行是实际的函数。
第12、16和20行每次都给half_of() 传递不同的实参。
第12行传递x其值为3.5 ;
第16行传递y,其值为65.11 ;
第20行传递z,其值为32.555。
x、y和z都被传入half_of的形参k中。这类似于分别将x、y和z的值拷贝给k。
然后,half_of()将返回该值除以2 (第31行) 的商。
程序运行后,将在屏幕上分别打印出正确的数字。
注意:程序中最后一次函数调用(第20行)说明,传递给函数的变量和接收函数返回值的变量可以相同。
也就是说,先把变量传递给函数,然后再用该变量接收函数返回的新值。 除非你习惯这样用,否则会对这样的用法很困感。
函数名要描述该函数的用途。 要确保传递给函数的实参数据类型与函数的形参数据类型相匹配。
不用给函数传递不必要的值。 传递给函数的实参个数不能少于形参的个数。
在c语言程序中,传入函数的实参个数必须与函数的形参个数相匹配。
4.2 函数体:
函数体位于函数头后面的花括号中。
函数的实际工作都是在函数体中完成。
调用函数时,从函数体的顶部开始执行,直至return语句或最外层的右花括号结束(返回调用程序)。
(1)局部变量
可以在函数体中声明变量。声明在函数中的变量称为局部变量。
“局部”意味着该变量归特定函数私有,与程序中声明在别处的同名变量不同。
现在,先来学习如何声明局部变量。
声明局部变量与声明其他变量一样,使用第3节学过的变量类型(可以在函数中声明任意C语言类型的变量)和命名规则。
可以在声明时初始化局部变量。
下面的示例在一个函数中声明了4个局部变量:
int func1 (int y)
{
int a, b = 10;
float rate;
double cost = 12.55;
//函数的其他代码已省略...
}
上面的示例中,声明了局部变量a、b、rate和cost,这些变量只能用于该函数。
注意:函数的形参可视为变量声明,因此,如果函数有形参的话,还可以在该函数中使用形参列表中的变量。
在函数中声明的变量,完全独立于程序其他部分声明的变量(即使这些变量与该变量同名)。
下面程序中说明了这一点。
输入:
//解释局部变量
#include <stdio.h>
int x = 1, y = 2;
void demo(void);
int main(void)
{
printf("\nBefore calling demo(), x = %d and y = %d.", x, y);
demo();
printf("\nAfter calling demo(), x =%d and y = %d\n.", x, y);
return 0;
}
void demo(void)
{
//声明并初始化两个局部变量
int x = 88, y = 99;
//Display their values.
printf("\nWithin demo(), x = %d and y = %d.", x, y);
}
输出:
解析:
程序中和前两个程序类似。
第5行声明x变量和y变量。
这两个变量在所有函数的外部声明,因此它们是全局变量。
第7行是demo()的函数原型,该函数没有形参也没有返回值,因此形参列表和返回类型都是void。
main() 函数非常简单,开始于第9行。
首先,第11行调用printf() 函数打印全局变量x和y的值,然后调用demo()函数。
注意,第22行demo() 声明了自己的局部变量x和y第26行打印的是局部变量x和y的值。
调用demo()函数后,第13行再次打印x和y的值,因为此时已离开demo()函数,所以打印的是全局变量x和y的值。
从程序中可知,函数中的局部变量x和y完全独立于函数外部的全局变量x和y。
在函数中使用变量要遵循以下3条规则:
●要在函数中使用变量,必须先在函数头或函数体中声明变量(全局变量除外) ;
●要在函数中获得调用程序中的值,必须将该值作为实参传递给函数;
●要在调用程序中获得函数中的值,必须将该值从函数中显式返回;
坦白地说,这些规则应用起来并不严格,后面将介绍如何避开它们。
尽管如此,现在先遵循这些规则,避免不必要的麻烦。
让函数独立的一个方法便是将函数的变量与程序的其他变量分离。
函数可以使用自己的一组局部变量完成任何数据操作,完全不会影响程序的其他部分。
另外,在函数中使用局部变量,更容易把函数应用到完成相同任务的新程序中。
(2)函数语句:
在函数中唯一不能做的是定义其他函数。
在函数中可以使用任何C语句,包括循环、if语句和赋值表达式语句。
除此之外,还可以调用库函数和其他用户自定义函数。
C语言是否对函数的长度有要求?
C语言没有严格规定函数的长度,但是考虑到实用性,应尽量让函数都比较简短。
记住,在结构化程序设计中,每个函数都应该只完成一个较简单的任务。
如果发现函数越来越长,那很可能在一个函数中执行了太多任务,可将其拆分为多个更小的函数。
那么多长的函数算太长?
这个问题没有明确的答案,但是根据实际的编程经验,很少有代码行超过25至30行以上的函数。
代码行太多通常意味着该函数要完成的任务过多。你必须学会自己判断,有些任务需要较长的函数才能完成,而有些只需要几行。
随着你的编程经验越来越丰富,在判断一个函数是否应该拆分为更小的函数时会越来越得心应手。
(3)返回值:
从函数返回值,要使用return关键字,后面是C语言的表达式。
程序执行到return语句时,将对表达式求值,然后把计算结果传回调用程序。函数的返回值就是表达式的值。
考虑下面的函数:
int func1
(int var)
{
int x;
/* 其他函数代码已省略... */
return x;
}
调用func1 ()函数时,将执行函数体中的语句直至return语句。
return 语句结束函数,并将x的值返回调用程序。
关键字return右边的表达式可以是任何有效的C表达式。
一个函数可包含多个return语句,但是只有第1个被执行的return语句有效。
要从函数中返回不同的值,使用多条return语句是行之有效的方法。
如下面程序所示:
输入:
/* 在一个函数中使用多个return语句示例 */
#include <stdio.h>
char last_init;
int room;
int room_assign(char last_init);
int main(void)
{
puts("Enter the first initial of you last name: ");
scanf("%c", &last_init);
room = room_assign(last_init);
printf("\nYou need to report to room %d.", room);
return 0;
}
int room_assign(char li)
{
//该if语句测试首字母是在A~M(a~m)之间还是在N~Z(或n~z)
//如果在A~M或a~m之间,则分配至1045房间,其余的分配至1055房间
//||用于检查首字母的大小写
if ((li > 'a' && li <= 'm') || (li >= 'A' && li <= 'M'))
return 1045;
else
return 1055;
}
输出1:
输出2:
解析:
和其他程序类似,该程序中开头的注释描述了该程序的用途(第1行)。
为了在程序中使用标准输入/输出函数在屏幕上显示信息和获取用户的输入,程序必须包含stdio.h头文件。
第8行是room_assign() 的函数原型。
注意,该函数需要一个char类型的参数,并返回int类型的值。
第15行调用带last_init实参的room_assign() 函数。
该函数包含多个return语句,而且利用if语句检查1i(main() 中的实参last_init 被传递给函数中的形参li)是否在“A”~“M"或者“a”~“m” 之间。
如果是,便执行第30行的return语句,然后结束函数。
在这种情况下,程序将忽略第31行和第32行。
如果li不在“a”~“m”之间(即,在“n”~“z”之间),则执行else子句中的return语句(第32行)。
你应该可以看出,该程序根据传入room_assign() 函数的实参来判断应执行哪个return语句并返回合适的值。
最后需要注意一点:
第12行使用了一个新的函数——puts() ,该函数名的意思是(放置字符串)。
这是一个简单的函数,用于在标准输出(通常是计算机屏幕)上显示字符串 (字符串将在后面中介绍,现在,只需知道字符串就是用双引号括起来的文本)。
记住,在函数头和函数原型中已经指定了函数的返回值类型。
函数的返回值必须与指定的类型相匹配,否则,编译器将生成错误消息。
注意: 结构化程序设计建议函数只有一个入口和一个出口。
这意味着,每个函数应尽量只包含一条return语句。
然而,有时包含多条return语句的程序也许更容易阅读和维护。
在某些情况下,应该优先考虑可维护性。
4.3函数原型:
每个函数都要有函数原型才能使用。前面介绍的程序中有许多函数原型的例子,如程序中的第4行。
函数原型到底是什么?为什么需要函数原型?
答:从外观上看,除了末尾的分号,函数原型与函数头完全相同;
从内容上看,函数原型与函数头一样,同样包含函数的返回类型、函数名和形参的信息。
函数原型的工作是将函数的基本情况告知编译器。
编译器通过函数原型提供的函数返回类型、函数名和形参的信息,在每次源代码调用函数时进行检查,核实传递的实参数量、类型以及返回值是否正确。如果其中一项不匹配,编译器便会生成错误消息。
严格地说,并不要求函数原型与函数头的内容精确匹配。只要函数原型的形参类型、数量和顺序与函数头相匹配,其形参名可以不同。尽管如此,还是建议保持函数头与函数原型各项相同,这样编写的源代码不仅更容易理解,而且在写好函数定义后,只需剪切并粘贴函数头便创建了函数原型(别忘了在函数原型末尾加分号)。
函数原型应放在源代码中的什么位置?
函数原型应放在第1个函数的前面。
为了提高程序的可读性,最好将程序所有的函数原型都放在一个位置。
注意:
尽可能使用局部变量
尽量限制每个函数只完成单独的任务。
不要返回与函数返回类型不同的值。
不要让函数太长。如果函数过长,可尝试将其拆分为多个更小的任务。
如无必要,不要在一个函数中包含多条return语句。然而,有时包含多个return语句让代码更易理解。
五.给函数传递实参:
要给函数传递实参,可将实参放在函数名后的圆括号中。
实参的数量和类型必须与函数头和函数原型的形参匹配。
例如,如果定义的函数需要两个int类型的实参,那么必须传递两个int类型的实参(不能多不能少,也不能是其他类型)。
如果给函数传递的实参数量或类型不匹配,编译器会根据函数原型中的信息检测出来。
如果函数需要多个实参,这些列于函数调用中的实参将被依次赋给函数的形参:
第1个实参赋给第1个形参,第2个实参赋给第2个形参,以此类推,如下图所示。
实参可以是任何有效的C表达式:常量、变量、数学表达式、逻辑表达式、甚至其他有返回值的函数。
例如,如果half() 、sequare()和third() 都是有返回值的函数,可以这样写:
x = half (third(square (half(y))));
程序首先调用half(),以y作为实参传递。从half()返回后,程序接着调用square(),把half()的返回值作为实参传递给square()。
接下来,调用third(),把square()的返回值作为实参传递给third()。
然后,再次调用half(),这次是把third()的返回值作为实参传递。
最后,将half()的返回值赋给x变量。
以下所列为等价的代码段:
a = half(y);
b = square(a);
c = third(b);
x = half(c);
六.调用函数:
调用函数有两种方法。
第1种方法是,在语句中直接使用函数名和实参列表( 即使函数有返回值,也不用写出来),
如下所示:
wait(12);
第2种方法只适用于有返回值的函数。
由于可以对这些函数求值(即,得到返回值),因此只要是可以使用C表达式的地方都可以使用这些函数。
前面介绍过带返回值的表达式可放在赋值表达式语句的右侧。下面来看示例。
下面这条语句中,half_of() 是printf() 函数的实参:
printf("Half of %d is %d.", x,half_of(x));
首先,调用带实参x的half_of() 函数,然后调用带实参"Half of%d is %d.", x和half_of(x) 的printf() 函数。
下面的示例,在一个表达式中使用了多个函数:
y = half_of(x) + half_of(z);
本例调用了两次half_of() ,当然,第2次调用的函数可以是任意其他有返回值的函数。
下面的代码与此例相同,但是3条语句各占一行:
a = half_of(x);
b = half_of(z);
y = a+b;
接下来介绍的两个示例是使用函数返回值的有效途径。在if语句中使用了一个函数:
if ( half_of(x) > 10 )
{
/*语句*/ /*可以是任何语句! */
}
如果函数的返回值符合条件(在本例中,即half_of() 返回的值大于10 ),则if语句为真,执行if块中的语句。如果函数的返回值不符合条件,则不执行if块中的语句。
这个示例更好:
if ( do_a_process() != OKAY )
{
//语句 //执行错误的例程
}
该例同样没有提供具体的语句,而且do_a_ process() 也不是一个真正的函数。但是,这个示例很重要。
该例检查do_a_process() 的返回值,判断该进程是否运行正常。如果不正常,则执行if块中的语句,处理错误或进行清理工作。
在文件中访问信息、比较值和分配内存时,经常会用到类似的处理方法。
警告:
如果将返回值类型为void的函数作为表达式,编译器会生成一条错误消息。
给函数传递参数,提高函数的通用性和复用性。要充分利用可将函数放在表达式中的功能。
不要在一条语句中包含太多函数,以免引起混淆。只有不会引起混淆才可把函数放入语句中。
6.1递归:
递归指的是在一个函数中直接或间接地调用自己。如果一个函数调用另一个函数,而后者又调用前者,将发生间接递归。
C语言允许递归函数,它们在一些特定的情况下很有用。
例如,递归可用于计算数的阶乘。数x的阶乘写作x!,计算方法如下:
x!,= x* (x-1) * (x-2) * (x-3) * ... * (2) * 1
还可以这样写:
x! = x * (x-1)!
用相同的方法进一步分析可知,计算(x-1)! 可以写作
(x-1)! = (x-1) * (x-2)!
下面程序使用递归函数计算阶乘。
由于程序中使用的是unsigned整型,因此输入的值最大为8,9的阶乘将超出unsigned整型的取值范围。
输入:
//函数递归示例.
//计算数的阶乘.
#include<stdio.h>
unsigned int f, x;
unsigned int factorial(unsigned int a);
int main(void)
{
puts("Enter an integer value between 1 and 8: ");
scanf("%d", &x);
if (x > 8 || x < 1)
{
printf("Only values from 1 to 8 are acceptable!");
}
else
{
f = factorial(x);
printf("%u factorial equals %u\n", x, f);
}
return 0;
}
unsigned int factorial(unsigned int a)
{
if (a == 1)
return 1;
else
{
a *= factorial(a - 1);
return a;
}
}
输出:
解析:
该程序示例的前半部分和其他程序类似。
为了使用输入/输出函数,第4行是是程序包含的头文件。
第6行声明unsigned类型的变量。
第7行是factorial函数的原型。注意,该函数需要一个unsigned int类型的参数,并返回一个unsigned int值。
第9-25行是main() 函数。
第11行打印一条信息,提示用户输入一个1~8之间的值,然后第12行接受用户输入的值。
第14~ 22行是if语句。
如果输入的值大于8会导致程序出错,因此该if语句用于检查输入值的有效性。
如果x大于8,将打印一条错误消息;
如果x在指定范围内,则计算x的阶乘(第20行),并打印出计算结果(第21行)。
递归函数factorial()在第27-36行。传入该函数的值将赋给a。
第29行,检查a的值。如果a的值是1,则返回1。如果a的值不是1,则将a与factorial (a-l)的乘积赋给a,再返回a。
程序将再次调用factorial ()函数, 但是这次a的值是(a-1)。
如果(a-1)不等于1,将会再次调用factorial()函数(此时a的值是(a-1)-1,即(a-2) )。
这个过程在if语句(第29行)为真之前将一直继续。如果用户输入的值是3,那么3的阶乘是:
如果用户输入的值是3,那么3的阶乘是:
3 * (3-1) * ((3-1)-1)
注意:
在程序中使用递归要理解递归的原理。
如果仅有几次迭代,不要使用递归。(迭代是指重复执行程序的语句)。因为函数要记住自己的位置,所以使用递归时要占用大量资源。
七.函数的位置:
你一定很想知道应该把函数定义放在源代码中的什么位置。
就现在而言,应该把它们都放在main()所在的源文件中,并位于main()的后面。
可以把自己的用户自定义函数放在一个独立的源代码文件中,与main()分离。
在大型程序中或者要在多个程序中使用同一组函数时,经常会这样做。
八.内联函数:
在C语言中可以创建一种特殊类型的函数——内联函数。
内联函数通常都很短小。
编译器会尽量以最快的方式(即,将函数代码拷贝进主调函数内)执行内联函数。
待执行的代码段将会被放入主调函数中,故称之为内联
使用inline关键字即可将函数设置为内联。
下面的代码段便声明了一个内联函数toInches() :
inline int toInches(int Feed)
{
return (Feed * 12);
}
在调用toInches()函数时,编译器会尽量优化该函数,以提高运行速度。
虽然通常都认为编译器会将内联函数的代码移至主调函数中,但是并未保证编译器一定会这样做。
唯一肯定的是, 编译器会尽量优化使用该函数的代码。内联函数的用法与其他函数相同。
九:本次总结:
本次介绍了C程序设计的重要组成部分一函数。函数是执行特定任务的独立代码段。程序通过调用函数来完成某项任务。结构化程序设计(一种强调模块化、自上而下的程序设计方法)离不开函数。用结构化程序设计创建的程序更高效,而且程序员用起来也非常方便。
本次还介绍了函数由函数头和函数体组成。函数头包含函数的返回类型、函数名和形参的信息。函数体中包含局部变量声明和调用该函数时执行的C语句。最后,简要介绍了局部变量,即声明在函数中的变量。局部变量完全独立于程序在别处声明的变量。
问答题
1.如何从函数返回多个值?
许多情况下都需要从一个函数返回多个值,或者你想更改传递给函数的值,而且在函数结束后仍然有效。
2.怎样的函数名是好的函数名?
函数命名类似于变量命名。好的函数名应尽可能具体地描述该函数的用途。
3.递归是否还有其他用途?
许多统计计算都要用到阶乘。递归虽然是一种循环,却不同于循环。每次调用递归函数时,都会创建一组新的变量。
4.程序的第1个函数是否必须是main ()函数?
C标准并未规定程序的第1个函数必须是main ()函数,只规定了程序第1个执行的是main ()函数。
main ()函数可放在源文件的任意位置。为了方便定位和查找,大多数程序员都将main ()作为第1个函数或最后一个函数。
5.在编写C程序时是否要使用结构化程序设计?
是的
6.结构化程序设计的工作原理是什么?
结构化程序设计把复杂的编程任务划分为多个更容易处理的简单任务。
7.如何用C函数进行结构化程序设计?
把程序划分为多个更简单的任务后,便可编写函数来执行这些任务。
8.函数定义的第1行必须是什么?要包含什么内容?
函数定义的第1行必须是函数头。函数头包含函数名、函数的返回类型和形参列表。
9.函数可以返回多少个值?
函数可以返回一个值或不返回值。返回值可以是任意变量类型。第19课介绍如何从函数返回多个值。
10.如果函数没有返回值,应该声明该函数是什么类型?
没有返回值的函数的类型是void 。
11.函数定义和函数原型的区别是?
函数定义是完整的函数,包括函数头和函数的所有代码。函数定义决定了执行函数时进行哪些操作。
函数原型只有一行,与函数头完全一样。不同的是,函数原型的末尾有分号。函数原型告诉编译器函数的名称、返回类型和形参列表。
12.什么是局部变量?
声明在函数中的变量是局部变量。
13.局部变量有何特殊之处?
局部变量独立于程序中的其他变量。
14.main()函数应放在程序的什么位置?
程序的第1个函数应该是main()函数。
实操题
1.编写do_it()函数的函数头,该函数接受2个char类型的实参,并将float类型的值返回主调函数。
float do_it (char a,char b, char c)
在末尾加上分号就是do_it()的函数原型。函数头后面应该是用花括号括起来的函数代码。
2.编写print_a_number ()函数的函数头,该函数接受一个int类型的实参,无返回值。
void print_a_number( int a_number )
这是一个void函数。与题1一样,在函数头末尾加上分号就是函数原型。在实际的程序中,函数头后面应该是用花括号括起来的函数代码。
3.以下函数返回值的类型是什么?
a. int print_error( float err_nbr);
b. long read_record( int rec_nbr, int size );
a. int
b. long
4.编写一个函数接受两个数作为实参,并返回计算结果。
假设两个数都是整数,函数的返回值也是整数:
int product( int x, int y )
{
return (x * y);
}
5.编写一个函数,接受两个数作为实参。如果第2个数不是0,则将第1个数除以第2个数。(提示:使用if语句)
不要假设传入的值一定正确。因为0会导致程序出错,所以下面代码检查了传入的第二个值是否为0。这种方法只能防止函数低级的错误。函数将0返回主函数后结束,因为函数需要一个整型值。要真正避免除以0这种错误,应调用devide_em()
函数之前在程序中检查b的值。
int divide_em( int a, int b )
{
int answer = 0;
if( b == 0 )
answer == 0;
else
answer = a/b;
return answer;
}
6.编写一个函数,调用练习7和练习8的函数。
虽然下面的程序中使用了main()函数
,也可以使用其他函数。
第9,10,11行调用了两个函数。第13~16行打印值。
要运行改程序,必须在第19行加上,上面第四题和第五题的代码。
#include<stdio.h>
int main( void )
{
int number1 = 10,
number2 =5;
int x, y, z;
x = product( number1, number2 );
y = divide_em( number1, number2 );
z = divide_em( number1, 0 );
printf( "\nnumber1 is %d and number2 is %d", number1, number2 );
printf( "\nnumber1 * number2 is %d", x );
printf( "\nnumber1 / number2 is %d", y );
printf( "\nnumber1 / 0 is %d", z );
return 0;
}
直接运行则会出错如下缺少条件:
添加上述代码之后再运行测试成功运行:
7.编写一个程序,其中使用一个函数计算用户输入的5个float类型值的平均值。
// 计算用户输入的5个值得平均值
#include<sdto.h>
float v, w, x, y, z, answer;
float average(float a, float b, float c, float d, float e);
int main(void)
{
puts("Enter five numbers:");
scanf("%f%f%f%f%f", &v, &w, &x, &y, &z);
answer = averge(v, w, x, y, z);
printf("The average is %f.\n", answer);
return 0;
}
float average(float a, float b, float c, float d, float e)
{
return ((a+b+c+d+e)/5);
}
8.排错题。
#include <stdio.h>
void print_msg(void );
int main( void )
{
print_msg( "This is a message to print" );
return 0;
}
void print_msg ( void )
{
puts("This is a message to print" );
return 0;
}
有两个问题,第一个问题,print_msg()函数
声明为void类型
,却返回了一个值。应该删除return 0语句
第二个问题在第5行,调用print_msg()函数
时,传递了一个参数(字符串)。而该函数的原型表明,形参列表为void,因此不应该给他传递任何参数。修改如下
#include <stdio.h>
void print_msg(void );
int main( void )
{
print_msg();
return 0;
}
void print_msg ( void )
{
puts("This is a message to print" );
}
9.排错题。
int twice (int y);
{
return(2* y);
函数头末尾不应该有分号去掉即可。