C语言 第四章-函数

函数是用于实现特定功能的代码模块。

一、函数定义

1.什么是函数

函数是一段可以直接被另一段程序或代码引用的程序或代码,一般用于实现某个特定功能,是模块化程序设计的前提。

2.为什么要定义函数

C 语言要求,在程序中用到的所有函数,都必须”先定义后使用“。

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

  • 指定函数的名字
  • 指定函数返回值的类型
  • 指定函数参数的名字与类型
  • 指定函数的功能(即代码块)

3.定义函数的方法

(1) 自建函数

1
2
3
类型名 函数名(参数表){
函数体
}

如果不需要返回值,可以将类型名设为 void ,以表示函数无返回值。

(2) 库函数

对于 C 编译系统提供的库函数,是由编译系统事先定义好的,库文件中包括了对各函数的定义。程序员只需要用 #include 指令把有关的头文件复制过来即可。

(3) 空函数

1
2
3
类型名 函数名(){

}

在 C 语言中,可以定义空函数,它不会起任何作用。

为什么要定义一个空函数?

在程序设计的早期,往往只会编写基本的功能模块,此时可以在将来准备扩充功能的地方协商一个空函数,方便代码理解和功能扩充的同时也不会影响现有程序运行。

4.函数声明

在 C 语言中,编译器会从上至下编译。如果被调用的函数没有放在调用位置之前,当编译至调用位置时,编译系统对调用语句一无所知(即不知道函数名,也不知道参数的个数与类型),便会报错。可以将被调用函数写在前面,但这也带来了许多新的问题:

  • 当函数嵌套调用时,仍然很容易发生错误。
  • 主函数放在末尾,代码可读性较差

因此,可以用声明来很好地解决这个问题。函数声明又称函数原型,用简短的方式声明函数,之后在对函数进行定义,增加代码可读性。

1
返回值类型 函数名(参数表);

可以简单地照抄函数定义的首行,在后面加个分号即可。

二、函数调用

1.函数调用的形式

1
函数名(参数表);

如果函数有参数,调用时应传递数量正确、类型正确的值。

(1) 单独调用

1
fun();

(2) 出现在表达式中

此时函数的返回值即为函数的值

1
c = max(a, b);

(3) 作为另一个函数的实参

此时函数的返回值即为函数的值

1
d = max(a, max(b, c));

2.函数调用的过程

  • 形参作为局部变量,在函数被调用时由系统临时分配内存空间

  • 实参的值被复制给形参

  • 执行函数体内的代码

  • 通过 return 语句,函数返回值被传递回去

  • 调用结束,包括形参在内的局部变量被释放

3.函数的嵌套调用

在 C 语言中,函数是相互平行、独立的,可以嵌套调用,即可以在一个函数中调用另一个函数。

4.函数的递归调用

函数直接或间接地调用该函数本身,称为函数的递归调用。

具体请看:

算法笔记 递归

三、参数传递

1.形参和实参

(1) 形参

函数参数表中的参数,又称为“形式参数”、“虚拟参数”。

形参是局部变量,仅在函数内有效。

具体请看:

技术分享 全局变量、局部变量和静态局部变量

(2) 实参

在调用函数时,填入括号中的“输入值”,又称为“实际参数”。

实参可以是字面量、变量、函数返回值、计算结果。

2.数据传递

在调用函数时,主调函数和被调用函数之间有数据传递关系。在调用函数过程中,系统会把实参的值传递给形参,实际上是将实参的值复制一份给形参,是单向传递。

形参用于在函数中参与运算,只在函数调用期间有效,因此在函数中对形参的修改无法传递到形参之上。

修改的传递可以通过指针或 C++ 中的引用实现

3.数组作为函数参数

(1) 数组元素作为函数实参

数组元素可以作为函数实参,但不可以作为函数形参。用数组元素作为函数实参时,向形参传递的是数组元素的值。

1
2
3
4
5
int add(int x, int y){
return x + y;
}
···
int sum = add(a[1], a[2]);

(2) 一维数组作为函数参数

数组名可以作为函数实参,也可以作为函数形参。用数组元素作为函数实参时,向形参传递的是数组首元素的地址。

数组如何传入函数?

  • 函数参数表中的数组实际上是指针

  • 在函数中 sizeof(数组) 结果是一个 int 的大小,因为数组被当作指针传入;

    在主函数中 sizeof(数组) 结果是 n 个 int 的大小。

    因为被调用的函数无法知道数组到底有多少个元素,只能通过数组的类型判断数组每个单元的大小。可以通过另外增加一个数组大小参数或像字符串一样增加末尾标识解决这一问题。

  • 返回值类型 函数名(int 数组名[]);

    返回值类型 函数名(int *数组名);

    两种做法的效果完全相同,数组都可以被正常传入且使用。

  • 返回值类型 函数名(int 数组名[数组大小]);

    在函数定义时指定其大小是完全没有作用的,

(3) 多维数组元素作为函数实参

和一维数组元素作为函数实参类似。多维数组元素可以作为函数实参,但不可以作为函数形参。用多维数组元素作为函数实参时,向形参传递的是数组元素的值。

(4) 多维数组作为函数参数

和一维数组作为函数参数类似,但仅可以省略第一维大小。

1
2
3
返回值类型 函数名(数组类型 数组名[][第二维大小]);

返回值类型 函数名(数组类型 (*数组名)[第二维大小]);

为什么不能省略高维?

将二维数组当作参数的时候,必须指明所有维数大小或者省略第一维的,但是不能省略第二维或者更高维的大小,这是由编译器原理限制的。事实上,编译器是这样处理数组的:

设有数组 int a[m][n],如果要访问 a[i][j ]的值,编译器的寻址方式为:

1
&a[i][j]=&a[0][0]+i*sizeof(int)*n+j*sizeof(int); // 注意 n 为第二维的维数

因此,可以省略第一维的维数,不能省略其他维的维数,否则便无法寻址。

同样的,仅可在函数中通过地址修改对应内存的值,在函数中修改地址无法传递。

4.指针变量作为函数参数

函数的参数不仅可以是整形、浮点型、字符型等数据,还可以是指针类型。此时实参与形参之间单向传递地址。

5.为什么数组和指针作为函数参数时可以修改变量值

为什么数组和指针作为函数参数时可以修改值,而变量作为函数参数时,无法修改值?

在函数中,形参与实参的参数传递是值传递、单向传递。在函数中修改(被复制了实参的值的)形参,无法同步修改实参。而数组作为函数参数时,实际上是将数组首字母地址传递给形参指针,形参指针获得地址后可以对该地址指向的内存区域进行修改。但需要注意,因为是值传递、单向传递,所以在函数中对指针指向的修改无法同步修改实参。

四、函数返回

1.函数的值

通过 return 值 可以返回函数的返回值,它将作为函数的值参与运算。

对于返回值为 void 的函数(即没有返回值的函数),可以用单独的 return 结束函数。

2.返回值的类型

应在定义函数时就给出返回值的类型。

1
2
3
返回值类型 函数名(参数表){
函数体
}

3.类型一致性

return 值 中的值类型应该与定义函数时指定的类型相同。若不同,系统往往会自己进行类型转换,但这可能带来许多问题。

五、内部函数和外部函数

根据函数能否被其它源文件调用,将函数区分为局部函数和全局函数。

1.内部函数

1
static 返回值类型 函数名(参数表);

内部函数又称静态函数,只能被本文件中的其它函数调用。使用内部函数,可以使函数的作用域互不干扰,减少函数同名带来的问题。

2.外部函数

1
extern 返回值类型 函数名(参数表);

也可以省略 extern

1
返回值类型 函数名(参数表);

因此,C 语言中的函数默认都是外部函数,可以被其它文件调用。(但其它文件中仍需对其进行声明,加不加 extern 都可以)

1
2
3
4
5
6
7
8
9
10
11
file1:
#include <stdio.h>
void fun();
int main(){
fun();
}
------------------------
file2:
void fun(){
printf("Hello!\n");
}

参考