c-programming-chap7

If you have a procedure with 10 parameters, you probably missed some. [1]

[TOC]

函数

引言

函数这个概念源自数学。在其他语言中,函数也叫方法,过程等。所以,编程中函数与数学中的函数是不同的。

我们早在第二章就接触到函数这一概念—— main 函数。当时我煞费苦心的尝试用通俗的话向你们解释 main 函数的构成以及各部分的功能。但我们刚开始学编程,对函数一定不能有一个比较深刻的认识。这一节,我将带领大家走进函数,仔细推敲品味函数。从这一节开始,使用函数的思想将会伴随我们今后的编程生涯。

这一章,会教你如何编写函数,并且更加深入的理解 main 函数本身。

不使用函数VS使用函数

为什么要使用函数

  1. 一个C程序由一个或多个程序模块组成,每一个程序模块作为一个源程序文件。较大的程序,可分别放在若干个源文件中。这样便于分别编写和编译,提高调试效率。一个源程序文件可以为多个C程序共用。
  2. 一个源程序文件由一个或多个函数以及其他有关内容(如指令、数据声明与定义等)组成。一个源程序文件是一个编译单位,在程序编译时是以源程序文件为单位进行编译的,而不是以函数为单位进行编译的。
  3. C程序的执行是从main函数开始的,如果在main函数中调用其他函数,在调用后流程返回到main函数,在main函数中结束整个程序的运行。
  4. 所有函数都是平行的,即在定义函数时是分别进行的,是互相独立的。一个函数并不从属于另一个函数,即函数不能嵌套定义。函数间可以互相调用,但不能调用main函数。main函数是被操作系统调用的。
  5. 从用户使用的角度看,函数有两种。
  • 库函数,它是由系统提供的,用户不必自己定义,可直接使用它们。应该说明,不同的C语言编译系统提供的库函数的数量和功能会有一些不同,当然许多基本的函数是共同的。
  • 用户自己定义的函数。它是用以解决用户专门需要的函数。
  1. 从函数的形式看,函数分两类。
  • 无参函数。在调用无参函数时,主调函数不向被调用函数传递数据。
  • 有参函数。在调用函数时,主调函数在调用被调用函数时,通过参数向被调用函数传递数据。

函数的定义、声明、调用

在介绍函数的定义之前,让我们先来看 3 个简单定义的函数。

1. 3 个 简单的函数

① 计算平均值

假设计算两个 double 类型的数值的平均值。

1
2
3
4
5
6
7
8
9
10
11
double average(double x, double y){
return (x + y) / 2;
}

int main(void){

double x = 1.0, y = 2.0;
printf("%f", average(x, y));

return 0;
}
② 显示倒计数

不是每一个函数都有返回值:

1
2
3
4
5
6
7
8
9
10
11
void print_count(int n){
printf("T minus %d and counting\n", n);
}

int main(void){

for(int i = 10; i > 0; i--){
print_count(i);
}
return 0;
}
③ 显示双关语

不是每个函数都有参数:

1
2
3
4
5
6
7
8
9
10
void print_pun(){
printf("To C or not to C: that is a question\n");
}

int main(void){

print_pun();

return 0;
}

2. 函数定义

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

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

  1. 指定函数的名字,以便以后按名调用。
  2. 指定函数的类型,即函数返回值的类型。
  3. 指定函数的参数的名字和类型,以便在调用函数时向它们传递数据。对无参函数不需要这项。
  4. 指定函数应当完成什么操作,也就是函数是做什么的,即函数的功能。这是最重要的,是在函数体中解决的。
无参函数
1
2
3
4
返回类型 函数名(){
声明
语句
}
有参函数
1
2
3
4
返回类型 函数名(形式参数){
声明
语句
}
空函数
1
2
3
返回类型 函数名(){
//为空即可
}
函数的返回值

(1) 函数的返回值是通过函数中的return语句获得的
一个函数中可以有一个以上的return语句,执行到哪一个return语句,哪一个return语句就起作用。return语句后面的括号可以不要,如“return z;”与“return(z);”等价。return后面的值可以是一个表达式。

(2) 函数值的类型
函数值的类型在定义函数时指定。

(3) 在定义函数时指定的函数类型一般应该和return语句中的表达式类型一致
如果函数值的类型和return语句中表达式的值不一致,则以函数类型为准。对数值型数据,可以自动进行类型转换。即函数类型决定返回值的类型。

(4) 对于不带回值的函数,应当用定义函数为“void类型”(或称“空类型”)
这样,系统就保证不使函数带回任何值,即禁止在调用函数中使用被调用函数的返回值。此时在函数体中不得出现return语句。

形参(形式参数)

在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。

实参(实际参数)

函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。


形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。
每个形式参数前需要写明其类型,形参之间用逗号隔开。

形参实参举例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
int main()
{ int max(int x,int y); //对max函数的声明
int a,b,c;
printf("please enter two integer numbers:"); //提示输入数据
scanf("%d,%d",&a,&b); //输入两个整数
c=max(a,b); //调用max函数,有两个实参。大数赋给变量c
printf("max is %d\n",c); //输出大数c
return 0; }
int max(int x,int y) //定义max函数,有两个参数
{
int z; //定义临时变量z
z=x>y?x:y; //把x和y中大者赋给z
return(z); //把z作为max函数的值带回main函数
}

数据传递

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

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

3.函数声明

函数声明:(function declaration)在调用前声明每个函数使得编译器可以先对函数进行概要浏览,而函数的定义可以以后再给出。

函数的声明类似函数的第一行:

1
返回类型 函数名(形式参数列表);

为 average() 函数添加声明后程序的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13


int main(void){
double average(double x, double y); // 对average函数进行声明
double x = 1.0, y = 2.0;
printf("%f", average(x, y));

return 0;
}

double average(double x, double y){
return (x + y) / 2;
}

我们把这种函数声明称为函数原型(function prototype)。函数原型为如何调用函数提供了完整的描述:返回值类型,实参个数和类型。

4. 函数调用

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

函数调用注意事项[重要]

在一个函数中调用另一个函数(即被调用函数)需要具备如下条件:
(1) 首先被调用的函数必须是已经定义的函数(是库函数或用户自己定义的函数)。
(2) 如果使用库函数,应该在本文件开头用#include指令将调用有关库函数时所需用到的信息“包含”到本文件中来。
(3) 如果使用用户自己定义的函数,而该函数的位置在调用它的函数(即主调函数)的后面(在同一个文件中),应该在主调函数中对被调用的函数作声明(declaration)。 声明的作用是把函数名、函数参数的个数和参数类型等信息通知编译系统,以便在遇到函数调用时,编译系统能正确识别函数并检查调用是否合法。

函数调用示例

函数调用由函数名和实参列表组成,实参列表用圆括号括起来:

1
2
3
average(x, y);
print_count(i);
print_pun();

程序:判断素数

编写程序提示用户录入数,然后给出一条信息说明此数是否为素数。

1
2
Enter a number: 24
Not prime

把判断素数的实现写到另外一个函数中,此函数返回值为 true 就表示是素数,返回 false 表示不是素数。

参考程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<stdio.h>
#include<stdbool.h>

bool is_prime(int n) {

int divisor;

if (n <= 1)
return false;

for (divisor = 2; divisor * divisor <= n; divisor++) {
if (n % divisor == 0)
return false;
}

return true;
}


int main(void) {

int n;

printf("Enter a number: ");
scanf("%d", &n);

if (is_prime(n))
printf("Prime\n");
else
printf("Not Prime\n");

return 0;
}

main 函数中包含一个叫 n 的变量,is_prime 函数中也有一个叫 n 的变量。这两个变量是虽然同名,但是在内存中的地址不同,是完全不相同的。所以给其中一个变量赋新值不会影响另一个变量。下一章我们还会详细的讨论这个问题。

is_prime 函数中有多条 return 语句。但是任何一次函数调用只能执行其中一条 return 语句,这是因为执行 return 语句后函数就会返回到调用点。本节后面还会深入的学习 return 。

实参形参应用

形式参数:(parameter) 出现再函数的定义中

实际参数:(argument)出现在函数调用中的表达式。

在 C语言中,实际参数是通过值传递的:调用函数时,计算出每个实际参数的值并将它赋值给相应的形式参数。在函数执行的过程中,形式参数的改变不会影响实参的值,这是因为形式参数是实参的副本。从效果上来讲,每个形式参数初始化为相应的实参的值。

1. 数组型实际参数

数组经常被当作实际参数。当形式参数为一维数组时,可以(而且是通常情况下)不说明数组长度:

1
2
3
int f(int a[]){
...;
}
示例:数组求和
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int sum_array(int a[], int n);

int main(void){

int a[] = {1, 2, 3, 4, 5};
int len = sizeof(a) / sizeof(a[0]);//数组占内存总空间,除以单个元素占内存空间⼤⼩
int sum;

sum = sum_array(a, len);

return 0;
}

int sum_array(int a[], int len){

int ret = 0;

for(int i = 0; i < len; i++){
ret += a[i];
}

return 0;
}

注意1: 把数组名传递给函数时,不要在数组名的后面放置方括号:

1
sum_array(a[], len); //error

注意2: 虽然可以用运算符 sizeof计算出数组变量的长度,但是它无法给出数组类型的形式参数长度的正确答案:

1
2
3
4
5
6
int f(int a[]){

int len = sizeof(a) / sizeof(a[0]);; //这样是错误的,得到的结果永远是1

...;
}
数组变量作为函数参数的特性 【了解即可】

1) 数组无法检测传入的数组长度是否正确,所以:

  • 一个数组有 100 个元素,但是实际仅仅使用 50 个元素,实参可以只写 50:

    1
    sum_array(a, 50);

    函数甚至不会知道数组还有 50 个元素存在!

  • 如果实际参数给的比数组还要大,会造成数组越界,从而导致未定义行为

    1
    sum_array(a, 150);// wrong

2)在函数中改变数组型形式参数的元素,同时会改变实际参数的数组元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include<stdio.h>

void store_zero(int a[], int len){

for(int i = 0; i < len; i++){
a[i] = 0;
}
}
int main(void){

int a[3] = {1, 2, 3};

store_zero(a, sizeof(a) / sizeof(a[0]));

for(int i = 0; i < 3; i++){
printf("%d ", a[i]);
}

return 0;
}

//输出:
0 0 0
多维数组

多维数组的形式参数可以省略第一维的长度,比如a[][3]

但是,这样的方式不能传递具有任意列数的多维数组。幸运的是,我们通常可以通过使用指针数组的方式解决这一问题。

局部变量\全局变量

局部变量

定义在函数内部的变量称为局部变量(Local Variable),它的作用域仅限于函数内部, 离开该函数后就是无效的,再使用就会报错。例如:

1
2
3
4
5
6
7
8
int f1(int a){
int b,c; //a,b,c仅在函数f1()内有效
return a+b+c;
}
int main(){
int m,n; //m,n仅在函数main()内有效
return 0;
}

全局变量

在所有函数外部定义的变量称为全局变量(Global Variable),它的作用域默认是整个程序,也就是所有的源文件,包括 .c 和 .h 文件。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int a, b;  //全局变量
void func1(){
//TODO:
}

float x,y; //全局变量
int func2(){
//TODO:
}

int main(){
//TODO:
return 0;
}

实例

1. 根据长方体的长宽高求它的体积以及三个面的面积。

根据题意,我们希望借助一个函数得到三个值:体积 v 以及三个面的面积 s1、s2、s3。遗憾的是,C语言中的函数只能有一个返回值,我们只能将其中的一份数据,也就是体积 v 放到返回值中,而将面积 s1、s2、s3 设置为全局变量。全局变量的作用域是整个程序,在函数 vs() 中修改 s1、s2、s3 的值,能够影响到包括 main() 在内的其它函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int s1, s2, s3; //面积

int vs(int a, int b, int c){
int v; //体积
v = a * b * c;
s1 = a * b;
s2 = b * c;
s3 = a * c;
return v;
}

int main(){
int v, length, width, height;
printf("Input length, width and height: ");
scanf("%d %d %d", &length, &width, &height);
v = vs(length, width, height);
printf("v=%d, s1=%d, s2=%d, s3=%d\n", v, s1, s2, s3);

return 0;
}

递归

如果一个函数调用它本身,那么此函数就是递归的(recursive)。

有些编程语言极度依赖递归,而有些编程语言甚至不允许使用递归。C语言介于中间:它允许递归,但是大多数 C 程序员并不经常使用递归。

用递归计算 n! 的结果:

1
2
3
4
5
6
7
8
int fact(int n){
if(n <= 1){
return 1;
}
else{
return n * fact(n - 1);
}
}

为了了解递归的工作原理,一起来追踪下面这个语句的执行:

1
i = fact(3);
1
2
3
4
5
fact(3) 发现 3 不是小于等于 1 的,fact(3) 调用 
fact(2),此函数发现 2 不是小于等于 1 的,fact(2) 调用
fact(1) ,此函数发现 1 是小于等于 1 的,所以 fact(1) 返回 1,从而导致
fact(2) 返回 2 * 1 = 2,从而导致
fact(3) 返回 3 * 2 = 6

注意: 要理解 fact 函数最终传递 1 之前,未完成的 fact 函数是如何“堆积”的。在最终传递 1 的那一点上,fact 函数逐个解开,直到 fact(3) 的原始调用返回 6 为止。

上面的程序也可以简化为:

1
2
3
int fact(int n){
return n <= 1 ? 1 : n * fact(n - 1);
}

注意: n <= 1 就是终止条件,为了放置无限递归,所有的递归都应该有终止条件。

参考资料:《C语言程序设计:现代方法》

  1. 如果你写了一个需要10个参数的函数,你或许还漏了什么。Epigrams on Programming 编程警句

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!