函数及其调用

  数学中我们其实就见过函数的概念,⽐如:一次函数 y=kx+b ,k和b都是常数,给⼀个任意的x,就 得到⼀个y值。 其实在C语⾔也引⼊函数(function)的概念,有些翻译为:⼦程序,⼦程序这种翻译更加准确⼀些。 C语⾔中的函数就是⼀个完成某项特定的任务的⼀⼩段代码。这段代码是有特殊的写法和调⽤方法的。 C语⾔的程序其实是由⽆数个⼩的函数组合⽽成的,也可以说:⼀个⼤的计算任务可以分解成若干个较小的函数(对应较⼩的任务)完成。同时⼀个函数如果能完成某项特定任务的话,这个函数也是可以 复⽤的,提升了开发软件的效率。 在C语⾔中我们⼀般会⻅到两类函数:库函数和自定义函数

1.库函数

  库函数是由系统提供的,用户不必自己定义,可直接使用它们。库文件中包括了对各函数的定义。程序设计者不必自己定义,只须用#include指令把有关的头文件包含到本文件模块中即可。在有关的头文件中包括了对函数的声明。例如,在程序中若用到数学函数( sqrt,fabs,sin,cos等),就必须在本文件模块的开头写上: 

 #include<math.h> 

库函数只提供了最基本、最通用的一些函数,而不可能包括人们在实际应用中所用到的所有函数。程序设计者需要在程序中自己定义想用的而库函数并没有提供的函数。

库函数相关头文件:https://zh.cppreference.com/w/c/header

标准库函数的一般调用形式为:

(1) 出现在表达式里。例如求y=x^2.5+1.3,可以通过以下语句调用pow函数来求得:

y=pow(x,2.5)+1.3;

在这里,函数的调用出现在赋值号右边的表达式。又如:

for(printf(":");scanf("%d",&x),t=x;printf(":"));

在此,函数printf和scanf都作为表达式而出现在for语句后的一对圆括号中。

(2)作为独立的语句完成某种操作。例如以下调用:

printf("*****\n");

在printf函数调用之后加了一个分号,这就构成了一条独立的语句,完成在一行上输出五个星号的操作

2.函数的定义

定义函数应包含以下几个内容:

(1)指定函数的名字,以便以后按名调用。

(2)指定函数的类型,即函数返回值的类型。

(3)指定函数的参数的名字和类型,以便在调用函数时向它们传递数据。对无参函数不需要这项。

(4)指定函数应完成什么操作,也就是函数是干什么的,即函数的功能。

2.1定义函数的方法

2.1.1.定义无参函数

函数名后面的括号中是空的,没有任何参数。定义无参函数的一般形式为

类型名 函数名() 

{
函数体

}

类型名 函数名(void) 
{
函数体
}

例题1:用函数调用输出以下结果

#include<stdio.h>
int main()
{
	void print_star();//声明print_star函数
	void print_message(); //声明print_message函数
	print_star(); //调用print_star函数
	print_message();//调用print_message函数
	print_star(); //调用print_star函数
	return 0;
}
void print_star()
{
	printf("***************\n");
}
void print_message()
{
	printf("How are you?\n");
}

1.print_star和print_message函数为void类型,表示没有函数值,也就是说,执行这两个函数后不会把任何值带回main函数。

2.函数体包括声明部分语句部分。函数声明的作用是把有关函数的信息(函数名、函数类型函数参数的个数与类型)通知编译系统,以便在编译系统对程序进行编译时,在进行到main 函数调用print starO和print_message(时知道它们是函数而不是变量或其他对象。此外,还对调用函数的正确性进行检查(如类型、函数名、参数个数、参数类型等是否正确)。

2.1.2.定义有参函数

一般形式为:

类型名 函数名(形式参数列表)

{

函数体

}

函数体包括声明部分和语句部分。

举个例子:

int max(int x,int y)
{
int z;
z=x>y?x:y;
return(z);
}

这是一个求x和y二者中大者的函数,第1行第1个关键字int表示函数值是整型的。max 
为函数名。括号中有两个形式参数x和y,它们都是整型的。在调用此函数时,主调函数把
实际参数的值传递给被调用函数中的形式参数x和y。花括号内是函数体,它可以包括声
明部分和语句部分。声明部分包括对函数中用到的变量进行定义以及对要调用的函数进行声明。。利用“z=x>y?x:y:”语句求出z的值(z为x与y中大者).returh(2的作用是指定将z的值作为两数值(称函数返回值)带回到主调函数。在函数定义时拍定max函数为整型,即指定函数的值是整型的,今在函数体中定义z为整型,并将2的值作为数借返,这是-致的此时,函数max的值等于z。

2.1.3.定义空函数

在程序设计中有时会用到空函数,它的形式为

类型名 函数名() 

{ }

例如: 

void dummy() 
{ }

函数体是空的。调用此函数时,什么工作也不做,没有任何实际作用。在主调函数中如果有调用此函数的语句:

 dummy(); 

 表明“要调用dummy 函数”,而现在这个函数没有起作用。那么为什么要定义一个空函数呢?在程序设计中往往根据需要确定若干个模块,分别由一些函数来实现。而在第1阶段只设计最基本的模块,其他一些次要功能或锦上添花的功能则在以后需要时陆续补上。

  在编写程序的开始阶段,可以在将来准备扩充功能的地方写上一个空函数(函数名取将来采用的实际函数名(如用merge(),matproduct(),concatenate和shell等,分别代表合并、矩阵相乘、字符串连接和希尔法排序等),只是这些函数暂时还未编写好,先用空函数占一个位置,等以后扩充程序功能时用一个编好的函数代替它。这样做,程序的结构清楚,可读性好,以后扩充新功能方便,对程序结构影响不大。空函数在程序设计中常常是有用的。

3.函数的返回值

函数的值通过return语句返回,return语句的形式如下:

return 表达式;

也就是说,return语句中的表达式的值就是所求的函数值,此表达式值的类型必须与函数首部所说明的类型一致。若类型不一致,则以函数值的类型为准,由系统自动进行转换。
当程序执行到return语句时,程序的流程就返回到调用该函数的地方(通常称为退出调用函数),并带回函数值。在同一个函数内,可以根据需要,在多处出现return语句,在函数体的不同部位退出函数。但无论函数体中有多少个return语句, return语句只可能执行一次。
return语句中也可以不含表达式,这时必须定义函数为void类型,它的作用只是使流程返回到调用函数,并没有确定的函数值。
函数体内可以没有return语句,这时也必须定义函数为void类型,程序的流程就一直执行到函数末尾的“}”,然后返回调用函数,也没有确定的函数值带回。

4.函数的调用

4.1函数调用的形式

一般形式为

函数名(实参表列)

如果是调用无参函数,则”实参表列“可以没有,但括号不能省略;如果实参表列包含多个实参,则各参数间用逗号隔开。

按函数调用在程序中出现的形式和位置来分,有以下3种调用方式:

1.函数调用语句

把函数调用单独作为一个语句。如例题中的”printf_star;”,这时不要求函数带回值,只要求函数完成一定的操作。

2.函数表达式

函数调用出现在另一个表达式中,如“c=max(a,b);",max(a,b)是一次函数调用,它是赋值表达式中的一部分。这时要求函数带回一个确定的值以参加表达式的运算。例如:

c=2*max(a,b);

3.函数参数
函数调用作为另一个函数调用时的实参。例如: 

m=max(a,max(b,c)); 

其中,max(b,c)是一次函数调用,它的值是b和c二者中的“大者”,把它作为max另一次调用的实参。经过赋值后,m的值是a,b,c三者中的最大者。又如: 

printf ("%d",max (a,b)); 

也是把max(a,b)作为 printf 函数的一个参数。
调用函数并不一定要求包括分号(如 print_star();),只有作为函数调用语句才需要有分号。如果作为函数表达式或函数参数,函数调用本身是不必有分号的。不能写成

printf ("%d",max (a,b);); 


4.2 函数调用时的数据传递

1.形式参数和实际参数
在调用有参函数时,主调函数和被调用函数之间有数据传递关系。在定义函数时函数名后面括号中的变量名称为“形式参数”(简称“形参”)或“虚拟参数”。在主调函数中调用一个函数时,数名质面括号中的参数称为“实际参数”(简称“实参”)。实际参数可以是常量、变量或表达式。

2.实参和形参间的数据转换

在调用函数过程中,系统会把实参的值传递给被调用函数的形参。或者说,形参从实参得到一个值。该值在函数调用期间有效,可以参加该函数中的运算。

在调用函数过程中发生的实参和形参之间的数据传递称为"虚实结合”。

例题2:输入两个整数,要求输出其中值较大者。要求用函数来找到最大值

#include<stdio.h>
int main()
{
	int max(int x, int y);
	int a, b, c;
	printf("请输入两个整数:");
	scanf("%d%d", &a, &b);
	c = max(a, b);
	printf("最大数为%d", c);
	return 0;

}
int max(int x, int y)
{
int z;
z = x>y ? x : y;
return(z);
}

先定义max函数(注意:第一行的末尾无分号)。第一行定义了一个max函数,函数类型为int。指定两个形参x和y,形参的类型为int。

主函数中包含了一个函数调用max(a,b)。max后面括号内a和b是实参。a和b是在main函数中定义的变量,x和y是函数max的形式参数。通过函数调用,在两个函数之间发生数据传递,实参a和b的值传递给形参x和y,在max函数中把x和y中大者赋给变量z,z的值作为函数值返回main函数,赋给变量c。

注意:实参可以是常量、变量或表达式,例如:max(3,a+b),但要求他们有确定的值。在调用时将实参的值赋给形参。

4.3函数调用的过程

(1)在定义函数中指定的形参,在未出现函数调用时,它们并不占内在中的存储单在发生函数调用时,函数max 的形参才被临时分配内存单元。
(2)将实参的值传递给对应形参。
(3)在执行max函数期间,由于形参已经有值,就可以利用形参进行有关的运算(例加把x和y 比较,把x或y的值赋给z等)。
(4)通过return语句将函数值带回到主调函数。应当注意:返回值的类型与函数类型一致。
如果函数不需要返回值,则不需要return语句。这时函数的类型应定义为void类型。
(5)调用结束,形参单元被释放。注意:实参单元仍保留并维持原值,没有改变。如果在执行一个被调用函数时,形参的值发生改变,不会改变主调函数的实参的值。这是因为实参与形参是两个不同的存储单元。
注意:实参向形参的数据传递是“值传递”,单向传递,只能由实参传给形参,而不能
由形参传给实参。实参和形参在内存中占有不同的存储单元,实参无法得到形参的值。

4.4函数的返回值

通常,希望通过函数调用使主调函数能得到一个确定的值,这就是函数值(函数的返回值)。例如,在例2的主函数中有

c=max(a,b); 

从 max 函数的定义中可以知道:函数调用max(2,3)的值是3,max(5,3)的值是5,3和5就是这两个函数的返回值,赋值语句把函数的返回值赋给变量c。
下面对函数值作一些说明。
(1)函数的返回值是通过函数中的return语句获得的。 return语句将被调用函数中的一个确定值带回到主调函数中去,如果需要从被调用函数带回一个函数值(供主调函数使用),被调用函数中必须包含return语句。如果不需要从被调用函数带回函数值可以不要return语句。
一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个return 语句就起作用。return语句后面的括号可以不要,如“return z;”与“return(z):”等价。return后面的值可以是一个表达式。例如,例2中的函数max可以改写如下: 
max(int x, int y) 
return(x>y?x:y); 
这样的函数体更为简短,只用一个return语句就把求值和返回都解决了。
(2)函数值的类型。既然函数有返回值,这个值当然应属于某一个确定的类型,应当在定义函数时指定函数值的类型。例如下面是3个函数的首行: 

int max(float x,float y) //函数值为整型
char letter(char c1,char c2) //函数值为字符型
double min(int x, int y) //函数值为双精度型

注意:在定义函数时要指定函数的类型。
(3)在定义函数时指定的函数类型-般应该和return语句中的表达式类型一致。例如,例2中指定max函数值为整型,而变量z也被指定为整型,通过return语句把z的值作为max的函数值,由max带回主调函数。z的类型与max函数的类型是一致的,是正确的。
如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。

5.函数的嵌套调用

在定义函数时,一个函数内不能再定义另一个函数,即不能嵌套定义,但可以嵌套调用函数,
即在调用一个函数的过程中,又调用另一个函数。

假设我们计算某年某⽉有多少天?如果要函数实现,可以设计2个函数: • is_leap_year():根据年份确定是否是闰年 • get_days_of_month():调⽤is_leap_year确定是否是闰年后,再根据⽉计算这个⽉的天数

int is_leap_year(int y)
{
 if(((y%4==0)&&(y%100!=0))||(y%400==0))
 return 1;
 else
 return 0;
}
int get_days_of_month(int y, int m)
{
 int days[] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
 int day = days[m];
 if (is_leap_year(y) && m == 2)
 day += 1;
 
 return day;
}
int main()
{
 int y = 0;
 int m = 0;
 scanf("%d %d", &y, &m);
 int d = get_days_of_month(y, m);
 printf("%d\n", d);
 return 0;
}

这⼀段代码,完成了⼀个独⽴的功能。代码中反应了不少的函数调⽤:

• main 函数调⽤ scanf 、 printf 、 get_days_of_month

• get_days_of_month 函数调⽤ is_leap_year

未来的稍微⼤⼀些代码都是函数之间的嵌套调⽤,但是函数是不能嵌套定义的。


例题3:输人4个整数,找出其中最大的数。用函数的嵌套调用来处理。

#include<stdio.h>
int main()
{
	int max4(int a, int b, int c, int d);
	int a, b, c, d, max;
	printf("请输入四个整数:");
		scanf("%d%d%d%d", &a, &b, &c, &d);
		max = max4(a, b, c, d);
		printf("max=%d\n",max);
		return 0;
}
int max4(int a, int b, int c, int d)
{
	int max2(int a, int b);
	int m;
	m = max2(a, b);//调用max函数,得到a和b两个数中的大者,放在m中
	m = max2(m, c);//调用max函数,得到a,b,c三个数中的大者,放在m中
	m = max2(m, d);//调用max函数,得到a,b,c,d四个数中的大者,放在m中
	return m;//把m作为函数值带回main函数中
}
int max2(int a, int b)
{
	if (a >= b)
		return a;
	else
		return b;
}

6.函数的递归调用

在的调用函数的过程中又出现直接或间接地调用该函数本身,称为函数的递归调用。

例如:

int f(int x)

{

int y,z;

z=f(y);//在执行f函数的过程中又要调用f函数

return(2*z);

}

在调用函数f的过程中,又要调用f函数(本函数),这是直接调用本函数。

如果在调用f1函数过程中要调用f函数(本函数),这是直接调用本函数。

如果在调用f1函数过程中要调用f2函数,而在调用f2函数过程中又要调用f1函数,就是间接调用本函数。

例题4:有5个学生坐在一起,问第5个学生多少岁,他说比第4个学生大2岁。问第4个学生多少岁,他说比第3个学生大2岁。问第3个学生多少岁,他说比第2个学生大2岁。问第2个学生多少岁,他说比第1个学生大2岁。最后问第1个学生,他说是10岁。请问;第5个学生多少岁。

解题思路:

age(5)=age(4)+2

age(4)=age(3)+2

age(3)=age(2)+2

age(2)=age(1)+2

age(1)=10

可以用数学公式表述:

age(n)=10              (n=1)

age(n)=age(n-1)+2     (n>1)

可以看到,当n>1时,求每位学生的年龄的公式是相同的。因此可以用一个函数表示上述关系。显然,这是一个递归问题。由图可知,求解可分成两个阶段:第1阶段是"回溯”,即将第5个学生的年龄表示为第4个学生年龄的函数,表示为age(5)=age(4)十2。而第4 个学生的年龄仍然不知道,还要“回溯”到第3个学生的年龄,表示为age(4)=age(3)+ 2......直到第1个学生的年龄。此时age(1)已知等于10,不必再向前回溯了。然后开始第2 阶段,采用递推方法,从第1个学生的已知年龄推算出第2个学生的年龄(12岁),从第2个学生的年龄推算出第3个学生的年龄(14岁).....-直推算出第5个学生的年龄(18岁)为。也就是说,一个递归的问题可以分为“回溯”和“递推”两个阶段。要经历若干步才能求出最后的值。显而易见,如果要求递归过程不是无限制进行下去,必须具有一个结束递归过程的条件。例如,age(1)=10,就是使递归结束的条件。

#include<stdio.h>
int main()
{
	int age(int n);//对age函数的声明
	printf("NO.5,age:%d\n", age(5));//输出第5个学生的年龄
	return 0;
}
int age(int n)//定义递归函数
{
	int c;
	if (n == 1)//如果n等于1
		c = 10;//年龄为10
	else
		c = age(n - 1) + 2;//年龄是前一个学生的年龄加2
	return c;//返回函数
}

运行结果如下:

例题5:用递归方法求n!

#define   _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	int fac(int n);
	int n;
	int y;
	printf("请输入一个整数:");
	scanf("%d", &n);
	y = fac(n);
	printf("%d!=%d\n", n, y);
	return 0;
}
int fac(int n)
{
	int f;
	if (n < 0)
		printf("n<0,data error!");
	else if (n == 0 || n == 1)
		f = 1;
	else f = fac(n - 1) * n;
	return(f);

}

运行结果如下:

注意:程序中的变量为int型,如果用visualC++以及多数C编译系统为int型数据分配4个字节,能表示的最大数为2147483647,当n=12时,运行正常,如果输入13,得不到预期结果,因为求出的结果超出了int类型数据的最大值。可将f,y,fac函数定义为float或double型。

例题6:Hanoi(汉诺)塔问题。这是一个古典的数学问题,是一个用递归方法解题的典型例子。问题是这样的:古代有一个梵塔,塔内有3个座A,B,C。开始时A座上有64个盘子,盘子大小不等,大的在下,小的在上(见图)。有一个老和尚想把这64个盘子从 A座移到C座,但规定每次只允许移动一个盘,且在移动过程中在3个座上都始终保持大盘在下,小盘在上。在移动过程中可以利用B座。要求编程序输出移动盘子的步骤。

解题思路::假如有另外一个和尚能有办法将上面63个盘子从一个座移到另一座。那么问题就解决了。此时老和尚只须这样做: 
(1)命令第2个和尚将63个盘子从 A座移到B座;
(2)自己将1个盘子(最底下的、最大的盘子)从 A座移到C座;

(3)再命令第2个和尚将63个盘子从B座移到C座。如下图所示:

至此,全部任务成了。这就是递归方法,把移动64个盘子简化为移动63个盘子,难度减小了一些。但是,有一个问题实际上未解决:第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座上的过程,移动前的情况见图a。
(1)将A座上2个盘子移到B座上(借助C座),见图b。
(2)将A座上1个盘子移到C座上,见图c。
(3)将B座上2个盘子移到C座上(借助A座),见图d。

其中第(2)步可以直接实现。第(1)步又可用递归方法分解为·

将A座上1个盘子从A座移到C座;
将A座上1个盘子从A座移到B座;

将C座上1个盘子从C座移到B座。

第(3)步可以分解为
将B座上1个盘子从B座移到A座上;

将B座上1个盘子从B座移到C座上;

·将A座上1个盘子从A座移到C座上。将以上综合起来,可得到移动3个盘子的步骤为
A→C,A→B,C→B,A→C,B→A,B→C,A→C

共经历7步。由此可推出:移动n个盘子要经历(2”-1)步。如移4个盘子经历15步移5个盘子经历31步,移64个盘子经历(264一1)步。
由上面的分析可知:将n个盘子从A座移到C座可以分解为以下3个步骤:

(1)将A座上n-1个盘借助C座先移到B座上;
(2)把A座上剩下的一个盘移到C座上;

(3)将n-1个盘从B座借助于A座移到C座上。
上面第(1)步和第(3)步,都是把n-1个盘从一个座移到另一个座上,采取的办法是样的,只是座的名字不同而已。为使之一般化,可以将第(1)步和第(3)步表示为: 
将one座上n-1个盘移到two 座(借助three座)。只是在第(1)步和第(3)步中,0ne, two, three和A,B,C的对应关系不同。对第(1)步,对应关系是one对应A, twO对应B,three对应C。对第(3)步,是:one对应B,two 对应C, three对应A。
因此,可以把上面3个步骤分成两类操作: 
(1)将n-1个盘从一个座移到另一个座上(n>1)。这就是大和尚让小和尚做的工作,它是一个递归的过程,即和尚将任务层层下放,直到第64个和尚为止。
(2)将1个盘子从一个座上移到另一座上。这是大和尚自己做的工作。
 

#define   _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int main()
{
	void hanoi(int n, char one, char two, char three);//对hanoi函数的声明
	int m;
	printf("请输入盘子的数量:");
	scanf("%d", &m);
	printf("移动%d个盘子的步骤\n", m);
	hanoi(m, 'A', 'B', 'C');
	return 0;
}
void hanoi(int n, char one, char two, char three)//
{
	void move(char x, char y);
	if (n == 1)
		move(one, three);
	else
	{
		hanoi(n - 1, one, three, two);
		move(one, three);
		hanoi(n - 1, two, one, three);
	}
}
void move(char x, char y)
{
	printf("%c-->%c\n", x, y);
}

运行结果如下:

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值