版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。

函数概述

函数的分类

函数是C语言程序基本的组成单位,每个程序有且只能有一个主函数(main()),其他的函数都是子函数。按照不同的额分类标准,函数可以分成不同的类:
函数从是否有返回值,可以将函数分为又返回值和无返回值函数两种;函数从是否需要参数,可以将函数分为有参和无参函数两种。在函数定义及函数声明时的参数,称为形参;在函数调用时的参数,称为实参。
在C语言中,一个函数的函数体中(包括main()函数),不能再定义另一个函数,即不能嵌套定义。但是允许函数之间的相互调用,也允许嵌套调用、递归调用。

函数的声明和定义

这里需要理解两个概念(函数声明和函数定义):
函数的定义是指对函数功能的确立,包括指定函数名,函数值类型、形参及其类型以及函数体等,它是一个完整的、独立的函数单位。而函数的声明的作用则是把函数的名字,函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时进行对照检查(例如,函数名是否正确,实参与形参的类型和个数是否一致),它不包括函数体。
也就是说:声明是为了让编译器正确处理对声明变量和函数的引用;定义是一个给变量分配内存的过程,或者是说明一个函数具体干什么用。

函数的位置

在C语言程序中,一个函数的定义可以放在在任何位置,既可以在main()函数之前,也可以在main()函数之后。但其实这句话,仔细推敲是有很多的东西在的,还和函数的定义和声明有很大的关系。
下面以几个例子来说明它们之间的关系:
例子1:
#includeint main() { fun1(); return 0; } void fun1() { printf("Hello!\n"); }
此时程序出错,错误提示:fun1()函数未定义(假设外部返回int);fun1()函数重定义。
例子2:
#includevoid fun1() { printf("Hello!\n"); } int main() { fun1(); return 0; }
此时程序运行通过(即:在调用的函数前定义函数,此时可以不需要声明)。
例子3:
#includevoid fun1(void); int main() { fun1(); return 0; } void fun1() { printf("Hello!\n"); }
此时程序运行通过(即:在调用的函数前声明函数,函数定义可以任意位置;函数声明也可以通过#include文件来包含)。
例子4:
#includeint main() { int a; a=fun1(); return 0; } int fun1() { return 0; }
此时程序运行通过。这段程序中fun1()函数同样没有声明,同时也在main()函数之后,为什么就又能运行通过呢?
解释:在C语言中,函数在调用前不一定非要声明。如果没有声明,那么编译器会自己主动依照一种隐式声明的规则,为调用函数确定函数类型。而这个隐式声明的函数类型为int(这是编译器假设的)!这就意味着,程序中添加了一句隐式声明:
int fun1();
所以,例子4中的程序没有问题,运行通过。这个时候我们反过头来看例子1中的情况,由于没有声明,因为编译器发现一个不认识的函数调用,不知道该函数的返回类型,就假设为int类型(添加了一句返回值为int的隐式声明),等后面编译的时候编译器看到实际的函数,它认为有两个同名的函数,一个是文件中的函数,一个是编译器假设返回int的那个,函数重定义,运行出错。
如何去避免呢:有没有办法让编译器一开始就知道函数的返回值类型呢?为了防止编译器假设函数的返回类型,你可以显式地告诉它。告诉编译器函数会返回什么类型的语句就叫函数声明。
还是不太理解的可以参考链接万恶之源:C语言中的隐式函数声明。

函数位置的总结

如果是在main()函数之后进行函数定义的,那么调用时就一定要在main()函数之前进行声明。如果不进行函数声明,编译器会假设返回值为int(隐式声明),除非函数返回值也为int才没有问题;如果在自定义函数在main()函数之前定义的,就可以不需要进行声明。因为在调用之前,编译系统已经知道了被调函数的函数类型、参数个数、类型、顺序等。
为了规范,在C语言中,函数一定必须肯定要声明,先声明后使用!

函数的返回值

当函数被调用时,函数体内往往会执行一些执行语句,完成具体的操作。如果想将这些操作的结果返回给主调函数,那么就需要使用函数的返回值。
例子1:
#includevoid add(int a); int main() {	int a=0;	add(a);	printf("%d\n", a);	return 0; } void add(int a) {	a++; }
程序的返回值为0,也就是说外部函数的各种操作的结果,只能通过返回值给主调函数。
例子2:
#includevoid add(); int a = 0; int main() {	add();	printf("%d\n", a);	return 0; } void add() {	a++; }
程序的返回值为1,此时通过全局变量(绕过返回值),实现函数之间的数据通信和交换。
当然除了这些方法之外,还有一个办法可以完成将函数内操作的结果返回给主调函数,就是利用指针。不利用指针,函数的形参都是临时分配内存空间,出了函数就自动销毁。而指针,是针对地址而言的,相当于直接操作原本的数据。

函数的递归调用

如果在调用一个函数的过程中,又直接或间接地调用了该函数本身,这种形式称为函数的递归调用,而这个函数就称为递归函数。递归函数分为直接递归和间接递归两种。
直接递归就是函数在处理过程中又直接调用了自己;间接递归就是函数p调用函数q,而函数q又反过来调用函数p。
但是运行递归函数会无休止地调用自身,为了防止这一点,必须在函数体内有终止递归调用的手段。常用的办法是加上条件判断,满足某种条件之后就不再进行递归调用。怎么才能不进行递归呢?return关键字的使用,如果有返回值,直接return返回值;如果没有返回值,就直接return;就可以了。
递归的方法实现Fibonacci数列的第10个数(斐波那契数列:1 1 2 3 5 8 13 21 34……):
#includelong fibonacci(int n); int main() {	long y;	y = fibonacci(10);	printf("%d\n", y);	return 0; } long fibonacci(int n) {	if (n == 1 || n == 2) {	return 1;	}else {	return (fibonacci(n - 1) + fibonacci(n - 2));	} }
终止递归调用通常情况下,我们使用的是递归到最里层的值(return 初值),而不是选取在递归终点的时候return 终值。

函数的参数

函数定义是的参数称为形参,形参在函数未被调用时是没有确定值的,只是形式上的参数。函数调用时使用的参数称为实参。

基本数据类型作为函数参数

当函数参数为基本数据类型的时候,函数被调用前,形参不占用内存的存储单元;函数被调用以后,形参才被分配内存单元;函数调用结束后,形参所占用的内存也将被回收,被释放。
这就解释了上文:如果想将这些操作的结果返回给主调函数,那么就需要使用函数的返回值。

数组作为函数参数

数组可以作为函数的参数只用,进行数据传送。数组用作函数参数有两种形式:一种是把数组元素作为实参使用;另一种是把数组名作为函数的形参和实参使用。
数组元素,与普通的变量并无区别,因此它作为函数实参使用与普通变量是完全相同的;数组名作为函数参数时,则要求形参和相对应的实参都必须是类型相同的数组,都必须有明确的数组说明。
在用数组名作为函数参数时,不是进行值的传送,即不是把实参数组的每个元素的值都赋予形参数组的每个元素。因为实际上形参数组并不存在,编译系统不为形参数组分配内存。
那么,数据的传送是如何实现的呢?我们曾介绍道,数组名就是数组的首地址。因此在数组名作为函数参数时所进行的传送只是地址的传送,也就是说把是参数组的首地址赋予形参数组名。形参数组名取得到该首地址之后,也就等于拥有了实在的数组。实际上是形参数组和是参数组为同一数组,共同拥有一段内存区间。
同时,在函数形参上,允许不给出形参数组的额长度,或者用一个变量来表示数组的元素个数:
void order(int a[5]); //函数声明 int x[10]; order(x); //函数调用 void order(int a[]); //函数声明 int x[10]; order(x); //函数调用 void order(int a[],int n); //函数声明 int x[10]; order(x,10); //函数调用
这就有一个很有意思的点了:基本数据类型和数组分别作为函数参数,一个是分配内存单元,用完就销毁;一个是不分配内存单元,直接操作地址。这两种方式有什么区别呢?来看两个很简单的比较两个数的大小的例子:
利用基本数据类型:
#includevoid order(int a, int b); int main() {	int x, y;	scanf_s("%d %d", &x, &y);	order(x, y);	printf("previous:%d %d\n", x, y);	return 0; } void order(int a, int b) {	int t;	if (a > b) {	t = a;	a = b;	b = t;	}	printf("result:%d %d\n", a, b); }
这段程序运行的结果为:
5 3 result:3 5 previous:5 3 请按任意键继续. . .
利用数组:
#includevoid order(int a[]); int main() {	int x[2];	scanf_s("%d %d", &x[0], &x[1]);	order(x);	printf("previous:%d %d\n", x[0], x[1]);	return 0; } void order(int a[]) {	int t;	if (a[0] > a[1]) {	t = a[0];	a[0] = a[1];	a[1] = t;	}	printf("result:%d %d\n", a[0], a[1]); }
这段程序运行的结果为:
5 3 result:3 5 previous:3 5 请按任意键继续. . .
相比较之下,这就非常显而易见了。
在基本数据类型做函数参数时,所进行的值传送是单向的,即只能从实参传向形参,不能从形参传回实参。形参的初值和实参相同,而形参的值发生改变后,实参并不变化;在数组名做函数参数时,由于实际上形参和实参为同一数组,因此当形参数组发生变化时,实参数组也随之变化。也就是说,所进行的值传送是双向的。