【编程语言】C语言函数(包括:函数编写位置、数组作函数实参)

函数概述

函数的分类

函数是C语言程序基本的组成单位,每个程序有且只能有一个主函数(main()),其他的函数都是子函数。按照不同的额分类标准,函数可以分成不同的类:

  • 函数从是否有返回值,可以将函数分为又返回值和无返回值函数两种;
  • 函数从是否需要参数,可以将函数分为有参和无参函数两种。在函数定义及函数声明时的参数,称为形参;在函数调用时的参数,称为实参。

在C语言中,一个函数的函数体中(包括main()函数),不能再定义另一个函数,即不能嵌套定义。但是允许函数之间的相互调用,也允许嵌套调用、递归调用。

函数的声明和定义

这里需要理解两个概念(函数声明和函数定义):

函数的定义是指对函数功能的确立,包括指定函数名,函数值类型、形参及其类型以及函数体等,它是一个完整的、独立的函数单位。而函数的声明的作用则是把函数的名字,函数类型以及形参的类型、个数和顺序通知编译系统,以便在调用该函数时进行对照检查(例如,函数名是否正确,实参与形参的类型和个数是否一致),它不包括函数体。

也就是说:声明是为了让编译器正确处理对声明变量和函数的引用;定义是一个给变量分配内存的过程,或者是说明一个函数具体干什么用。


函数的位置

在C语言程序中,一个函数的定义可以放在在任何位置,既可以在main()函数之前,也可以在main()函数之后。但其实这句话,仔细推敲是有很多的东西在的,还和函数的定义和声明有很大的关系。

下面以几个例子来说明它们之间的关系:

例子1:

#include <stdio.h>

int main()
{
    fun1();
    return 0;
} 

void fun1()
{
    printf("Hello!\n");
}

此时程序出错,错误提示:fun1()函数未定义(假设外部返回int);fun1()函数重定义。

例子2:

#include <stdio.h>

void fun1()
{
    printf("Hello!\n");
}

int main()
{
    fun1();
    return 0;
}

此时程序运行通过(即:在调用的函数前定义函数,此时可以不需要声明)。

例子3:

#include <stdio.h>

void fun1(void);

int main()
{
    fun1();
    return 0;
} 

void fun1()
{
    printf("Hello!\n");
}

此时程序运行通过(即:在调用的函数前声明函数,函数定义可以任意位置;函数声明也可以通过#include文件来包含)。

例子4:

#include <stdio.h>

int 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:

#include<stdio.h>

void add(int a);

int main()
{
	int a=0;
	add(a);
	printf("%d\n", a);

	return 0;
}

void add(int a) {
	a++;
}

程序的返回值为0,也就是说外部函数的各种操作的结果,只能通过返回值给主调函数。

例子2:

#include<stdio.h>

void 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……):

#include<stdio.h>

long 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);            //函数调用

这就有一个很有意思的点了:基本数据类型和数组分别作为函数参数,一个是分配内存单元,用完就销毁;一个是不分配内存单元,直接操作地址。这两种方式有什么区别呢?来看两个很简单的比较两个数的大小的例子:

利用基本数据类型:

#include<stdio.h>

void 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
请按任意键继续. . .

利用数组:

#include<stdio.h>

void 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
请按任意键继续. . .

相比较之下,这就非常显而易见了。

  • 在基本数据类型做函数参数时,所进行的值传送是单向的,即只能从实参传向形参,不能从形参传回实参。形参的初值和实参相同,而形参的值发生改变后,实参并不变化;
  • 在数组名做函数参数时,由于实际上形参和实参为同一数组,因此当形参数组发生变化时,实参数组也随之变化。也就是说,所进行的值传送是双向的。


内部函数和外部函数

当一个源程序由多个源文件组成时,C语言根据函数能够被其他源文件中的函数调用,将函数分为内部函数和外部函数。

内部函数

如果在一个源文件中定义的函数只能被本文件中的函数调用,而不能被同一源程序其他文件中的函数调用,这种函数就称为内部函数。在定义内部函数时,需要在函数名和函数类型前面加static关键字。

内部函数定义的一般形式:

static 类型说明符 函数名([形参表])
{
    函数体;
}

内部函数又称为静态函数,但此处的静态的含义不是指存储方式,而是指对函数的作用于仅局限于本文件,因此在不同的源文件中定义同名的内部函数不会引起混淆。通常把只由同一个文件使用的函数和外部变量放在一个文件中,前面加上static使之局部化,其他文件不能引用。

外部函数

如果在一个源文件中定义的函数可以被同一源程序其他文件中的其它函数调用,这种函数称为外部函数。外部函数在整个源程序中都有效。在定义外部函数时需要在函数名和函数类型前面加上关键字extern。

外部函数定义的一般形式:

extern 类型说明符 函数名([形参表])
{
    函数体;
}

因为函数和函数之间都是并列的,函数不能嵌套定义,所以函数在本质上都具有外部性质。因此,在定义函数时,可以省去extern关键字,此时则隐含为外部函数。

在一个源文件中的函数中调用其他源文件中定义的外部函数时,通常使用extern声明被调用的函数是外部函数。

其他源文件中,外部函数声明的一般形式:

extern 类型说明符 函数名([形参表]);

也就是说,外部函数定义的时候,extern关键字可省略;其他源文件中,进行外部函数调用的时候,extern关键字声明不能省略。

猜你喜欢

转载自blog.csdn.net/qq_38410730/article/details/80190276