我们已经知道,函数简单来说就是一连串语句,这些语句被组合在一起,并被指定了一个名字。虽然“函数”这个术语来自数学,但是C语言的函数不完全等同于数学函数。在C语言中,函数不一定要有参数,也不一定要计算数值。(在某些编程语言中,“函数”需要返回一个值,而“过程”不返回值;C语言没有这样的区别。)
函数是C程序的构建块。每个函数本质上是一个自带声明和语句的小程序。可以利用函数把程序划分成小块,这样便于人们理解和修改程序。由于不必重复编写要多次使用的代码,函数可以使编程不那么单调乏味。此外,函数可以复用:一个函数最初可能是某个程序的一部分,但可以将其用于其他程序中。
到目前为止,我们的程序都只是由一个main函数构成的。本章将学习如何编写除main函数以外的其他函数,并更加深入地了解main函数本身。9.1节介绍定义和调用函数的方法;9.2节讨论函数的声明,以及它和函数定义的差异;接下来,9.3节讲述参数是怎么传递给函数的。余下的部分讨论return语句(9.4节)、与程序终止相关的问题(9.5节)和递归(9.6节)。
9.1函数的定义和调用
在介绍定义函数的规则之前,先来看3个简单的定义函数的程序。
程序计算平均值
假设我们经常需要计算两个double类型数值的平均值。C语言库没有“求平均值”函数,但是可以自己定义一个。下面就是这个函数的形式:
double average(double a, double b){
return (a + b) / 2;
}
在函数开始处放置的单词double表示average函数的返回类型(return type),也就是每次调用该函数时返回数据的类型。标识符a和标识符b(即函数的形式参数(parameter))表示在调用average函数时需要提供的两个数。每一个形式参数都必须有类型(正像每个变量有类型一样),这里选择了double作为a和b的类型。(这看上去有点奇怪,但是单词double必须出现两次,一次为a而另一次为b。)函数的形式参数本质上是变量,其初始值在调用函数的时候才提供。
每个函数都有一个用花括号括起来的执行部分,称为函数体(body)。average函数的函数体由一条return语句构成。执行这条语句将会使函数“返回”到调用它的地方,表达式(a+b)/2的值将作为函数的返回值。
为了调用函数,需要写出函数名及跟随其后的实际参数(argument)列表。例如,average(x, y)是对average函数的调用。实际参数用来给函数提供信息;在此例中,函数average需要知道是要求哪两个数的平均值。调用average(x, y)的效果就是把变量x和y的值复制给形式参数a和b,然后执行average函数的函数体。实际参数不一定要是变量,任何正确类型的表达式都可以,average(5.1, 8.9)和average(x/2, y/3)都是合法的函数调用。
我们把average函数的调用放在需要使用其返回值的地方。例如,为了计算并显示出x和y的平均值,可以写成
printf("Average: %g\n", average(x, y));
这条语句产生如下效果。
(1) 以变量x和y作为实际参数调用average函数。
(2) 把x和y的值复制给a和b。
(3) average函数执行自己的return语句,返回a和b的平均值。
(4) printf函数显示出函数average的返回值。(average函数的返回值成为了函数printf的一个实际参数。)
注意,我们没有保存average函数的返回值,程序显示这个值后就把它丢弃了。如果需要在稍后的程序中用到返回值,可以把这个返回值赋值给变量:
avg = average(x, y);
这条语句调用了average函数,然后把它的返回值存储在变量avg中。
现在把average函数放在一个完整的程序中来使用。下面的程序读取了3个数并且计算它们的平均值,每次计算一对数的平均值:
Enter three numbers: 3.5 9.6 10.2
Average of 3.5 and 9.6: 6.55
Average of 9.6 and 10.2: 9.9
Average of 3.5 and 10.2: 6.85
这个程序表明只要需要可以频繁调用函数。
average.c
/* Computes pairwise averages of three numbers */
#include <stdio.h>
double average(double a, double b){
return (a + b) / 2;
}
int main(void){
double x, y, z;
printf("Enter three numbers: ");
scanf("%lf%lf%lf", &x, &y, &z);
printf("Average of %g and %g: %g\n", x, y, average(x, y));
printf("Average of %g and %g: %g\n", y, z, average(y, z));
printf("Average of %g and %g: %g\n", x, z, average(x, z));
return 0;
}
注意,这里把average函数的定义放在了main函数的前面。在9.2节我们将看到,把average函数的定义放在main函数的后面可能会有问题。
程序 显示倒计数
不是每个函数都返回一个值。例如,进行输出操作的函数可能不需要返回任何值。为了指示出不带返回值的函数,需要指明这类函数的返回类型是void。(void是一种没有值的类型。)思考下面的函数,这个函数用来显示信息T minusand counting,其中的值在调用函数时提供:
void print_count(int n){
printf("T minus %d and counting\n", n);
}
函数print_count有一个形式参数n,参数的类型为int。此函数没有返回任何值,所以用void指明它的返回值类型,并且略掉了return语句。既然print_count函数没有返回值,那么不能使用调用average函数的方法来调用它。print_count函数的调用必须自成一个语句:
print_count(i);
下面这个程序在循环内调用了10次print_count函数:
countdown.c
/* Prints a countdown */
#include <stdio.h>
void print_count(int n){
printf("T minus %d and counting\n", n);
}
int main(void){
int i;
for (i = 10; i > 0; --i)
print_count(i);
return 0;
}
最开始,变量i的值为10。第一次调用print_count函数时,i被复制给n,所以变量n的值也是10。因此,第一次调用print_count函数会显示
T minus 10 and counting
随后,函数print_count返回到被调用的地方,而这个地方恰好是for语句的循环体。for语句再从调用离开的地方重新开始,先让变量i自减变成9,再判断i是否大于0。由于判断结果为真,因此再次调用函数print_count,这次显示
T minus 9 and counting
每次调用print_count函数时,变量i的值都不同,所以print_count函数会显示10条不同的信息。
程序显示双关语(改进版)
一些函数根本没有形式参数。思考下面这个print_pun函数,它在每次调用时显示一条双关语:
void print_pun(void){
printf("To C, or not to C: that is the question.\n");
}
在圆括号中的单词void表明print_pun函数没有实际参数。(这里使用void作为占位符,表示“这里没有任何东西”。)
调用不带实际参数的函数时,只需要写出函数名并且在后面加上一对圆括号:
print_pun();
即使没有实际参数也必须给出圆括号。
下面这个小程序测试了print_pun函数:
pun2.c
/* Prints a bad pun */
#include <stdio.h>
void print_pun(void){
printf("To C, or not to C: that is the question.\n");
}
int main(void)
{
print_pun();
return 0;
}
程序首先从main函数中的第一条语句开始执行,这里碰巧第一句就是print_pun函数调用。开始执行print_pun函数时,它会调用printf函数显示字符串。当printf函数返回时,print_pun函数也就返回到了main函数。
9.1.1 函数定义
现在已经看过了一些例子,该来看看函数定义的一般格式了。
[函数定义]
返回类型函数名 (形式参数){声明语句}
函数的“返回类型”是函数返回值的类型。下列规则用来管理返回类型。
- 函数不能返回数组,但关于返回类型没有其他限制。
- 指定返回类型是void类型说明函数没有返回值。
- 如果省略返回类型,C89会假定函数返回值的类型是int类型,
一些程序员习惯把返回类型放在函数名的上边:
double
average(double a, double b){
return (a + b) /2;
}
如果返回类型很冗长,比如unsigned long int类型,那么把返回类型单独放在一行是非常有用的。
函数名后边有一串形式参数列表。需要在每个形式参数的前面说明其类型,形式参数间用逗号进行分隔。如果函数没有形式参数,那么在圆括号内应该出现void。注意:即使几个形参具有相同的数据类型,也必须对每个形式参数分别进行类型说明。
double average(double a, b) /*** WRONG ***/{
return (a + b) /2;
}
函数体可以包含声明和语句。例如,average函数可以写成
double average(double a, double b){
double sum; /* declaration */
sum = a + b; /* statement */
return sum / 2; /* statement */
}
函数体内声明的变量专属于此函数,其他函数不能对这些变量进行检查或修改。在C89中,变量声明必须出现在语句之前。在C99中,变量声明和语句可以混在一起,只要变量在第一次使用之前进行声明就行。(C99之前的有些编译器也允许声明和语句混合。)
对于返回类型为void的函数(本书称为“void函数”),其函数体可以为空:
void print_pun(void){
}
程序开发过程中留下空函数体是有意义的。由于没有时间完成函数,所以为它预留下空间,以后可以回来编写它的函数体。
9.1.2 函数调用
函数调用由函数名和跟随其后的实际参数列表组成,其中实际参数列表用圆括号括起来:
average(x, y)
print_count(i)
print_pun()
如果丢失圆括号,那么将无法进行函数调用:
print_pun; /*** WRONG ***/
这样的结果是合法的(虽然没有意义)表达式语句,而且看上去这语句是正确的,但是这条语句不起任何作用。一些编译器会发出一条类似“statement with no effect”的警告。
void函数调用的后边始终跟着分号,使得该调用成为语句:
print_count(i);
print_pun();
另一方面,非void函数调用会产生一个值,该值可以存储在变量中,还可以进行测试、显示或者用于其他用途:
avg = average(x, y);
if (average(x, y) > 0)
printf("Average is positive\n");
printf("The average is %g\n", average(x, y));
如果不需要非void函数返回的值,总是可以将其丢弃:
average(x, y); /* discards return value */
average函数的这个调用就是一个表达式语句(➤4.5节)的例子:语句计算出值,但是不保存它。
当然,丢掉average函数的返回值是很奇怪的一件事,但在有些情况下是有意义的。例如,printf函数返回显示的字符个数。在下面的调用后,变量num_chars的值为9:
num_chars = printf ("Hi, Mom!\n");
因为我们可能对显示出的字符数量不感兴趣,所以通常会丢掉printf函数的返回值:
printf("Hi, Mom!\n"); /* discards return value */
为了清楚地表明函数返回值是被故意丢掉的,C语言允许在函数调用前加上(void):
(void) printf ("Hi, Mom!\n");
我们所做的工作就是把printf函数的返回值强制类型转换(➤7.4节)成void类型。(在C语言中,“强制转换成void”是对“抛弃”的一种客气说法。)使用(void)可以使别人清楚编写者是故意抛弃返回值的,而不是忘记了。但是,C语言库中大量函数的返回值通常都会被丢掉;在调用它们时都使用(void)会很麻烦,所以本书没有这样做。
程序 判定素数
为了弄清楚函数如何使程序变得更加容易理解,现在来编写一个程序用以检查一个数是否是素数。这个程序将提示用户录入数,然后给出一条消息说明此数是否是素数:
Enter a number: 34
Not prime
我们没有在main函数中加入素数判定的细节,而是另外定义了一个函数,此函数返回值为true就表示它的形式参数是素数,返回false就表示它的形式参数不是素数。给定数n后,is_prime函数把n除以从2到n的平方根之间的每一个数;只要有一个余数为0,n就不是素数。
prime.c
/* Tests whether a number is prime */
#include <stdbool.h> /* c99 only */
#include <stdio.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。一般来说,在一个函数中可以声明与另一个函数中的变量同名的变量。这两个变量在内存中的地址不同,所以给其中一个变量赋新值不会影响另一个变量。(形式参数也具有这一性质。)10.1节会更详细地讨论这个问题。
如is_prime函数所示,函数可以有多条return语句。但是,在任何一次函数调用中只能执行其中一条return语句,这是因为到达return语句后函数就会返回到调用点。在9.4节我们会更深入地学习return语句。
9.2 函数声明
在9.1节的程序中,函数的定义总是放置在调用点的上面。事实上,C语言并没有要求函数的定义必须放置在调用点之前。假设重新编排程序average.c,使average函数的定义放置在main函数的定义之后:
#include <stdio.h>
int main(void){
double x, y, z;
printf("Enter three numbers: ");
scanf("%lf%lf%lf", &x, &y, &z);
printf("Average of %g and %g: %g\n", x, y, average(x, y));
printf("Average of %g and %g: %g\n", y, z, average(y, z));
printf("Average of %g and %g: %g\n", x, z, average(x, z));
return 0;
}
double average (double a, double b){
return (a + b) / 2;
}
当遇到main函数中第一个average函数调用时,编译器没有任何关于average函数的信息:编译器不知道average函数有多少形式参数,形式参数的类型是什么,也不知道average函数的返回值是什么类型。但是,编译器不会给出出错消息,而是假设average函数返回int型的值(回顾9.1节的内容可以知道函数返回值的类型默认为int型)。我们可以说编译器为该函数创建了一个隐式声明(implicit declaration)。编译器无法检查传递给average的实参个数和实参类型,只能进行默认的实际参数提升(➤9.3节)并期待最好的情况发生。当编译器在后面遇到average的定义时,它会发现函数的返回类型实际上是double而不是int,从而我们得到一条出错消息。
为了避免定义前调用的问题,一种方法是使每个函数的定义都出现在其调用之前。可惜的是,有时候无法进行这样的安排;而且即使可以这样安排,程序也会因为函数定义的顺序不自然而难以阅读。
幸运的是,C语言提供了一种更好的解决办法:在调用前声明每个函数。函数声明(function declaration)使得编译器可以先对函数进行概要浏览,而函数的完整定义以后再给出。函数声明类似于函数定义的第一行,不同之处是在其结尾处有分号:
[函数声明] 返回类型函数名 (形式参数);
无需多言,函数的声明必须与函数的定义一致。
下面是为average函数添加了声明后程序的样子:
#include <stdio.h>double average(double a, double b); /* DECLARATION */
int main(void){
double x, y, z;
printf("Enter three numbers: ");
scanf("%lf%lf%lf", &x, &y, &z);
printf("Average of %g and %g: %g\n", x, y, average(x, y));
printf("Average of %g and %g: %g\n", y, z, average(y, z));
printf("Average of %g and %g: %g\n", x, z, average(x, z));
return 0;
}
double average(double a, double b) /* DEFINITION */{
return (a + b) / 2;
}
为了与过去的那种圆括号内为空的函数声明风格相区别,我们把正在讨论的这类函数声明称为函数原型(function prototype)。原型为如何调用函数提供了完整的描述:提供了多少实际参数,这些参数应该是什么类型,以及返回的结果是什么类型。
顺便提一句,函数原型不需要说明函数形式参数的名字,只要显示它们的类型就可以了:
double average(double, double);
通常最好是不要省略形式参数的名字,因为这些名字可以说明每个形式参数的目的,并且提醒程序员在函数调用时实际参数的出现次序。当然,省略形式参数的名字也有一定的道理,有些程序员喜欢这样做。
C99遵循这样的规则:在调用一个函数之前,必须先对其进行声明或定义。调用函数时,如果此前编译器未见到该函数的声明或定义,会导致出错。
9.3 实际参数
复习一下形式参数和实际参数之间的差异。形式参数(parameter)出现在函数定义中,它们以假名字来表示函数调用时需要提供的值;实际参数(argument)是出现在函数调用中的表达式。在形式参数和实际参数的差异不是很重要的时候,有时会用参数表示两者中的任意一个。
在C语言中,实际参数是通过值传递的:调用函数时,计算出每个实际参数的值并且把它赋值给相应的形式参数。在函数执行过程中,对形式参数的改变不会影响实际参数的值,这是因为形式参数中包含的是实际参数值的副本。从效果上来说,每个形式参数的行为好像是把变量初始化成与之匹配的实际参数的值。
实际参数按值传递既有利也有弊。因为形式参数的修改不会影响到相应的实际参数,所以可以把形式参数作为函数内的变量来使用,这样可以减少真正需要的变量的数量。思考下面这个函数,此函数用来计算数x的n次幂:
int power(int x, int n){
int i, result = 1;
for (i = 1; i <= n; i++)
result = result * x;
return result;
}
因为n只是原始指数的副本,所以可以在函数体内修改它,因此就不需要使用变量i了:
int power(int x, int n){
int result = 1;
while (n-- > 0)
result = result * x;
return result;
}
可惜的是,C语言关于实际参数按值传递的要求使它很难编写某些类型的函数。例如,假设我们需要一个函数,它把double型的值分解成整数部分和小数部分。因为函数无法返回两个数,所以可以尝试把两个变量传递给函数并且修改它们:
void decompose(double x, long int_part, double frac_part){
int_part = (long) x; /* drops the fractional part of x */
frac_part = x - int_part;
}
假设采用下面的方法调用这个函数:
decompose(3.14159, i, d);
在调用开始,程序把3.14159复制给x,把i的值复制给int_part,而且把d的值复制给frac_part。然后,decompose函数内的语句把3赋值给int_part而把.14159赋值给frac_part,接着函数返回。可惜的是,变量i和d不会因为赋值给int_part和frac_part而受到影响,所以它们在函数调用前后的值是完全一样的。正如在11.4节将会看到的那样,稍做一点额外的工作就可以使decompose函数工作。但是,我们首先需要介绍更多C语言的特性。
实际参数的转换
C语言允许在实际参数的类型与形式参数的类型不匹配的情况下进行函数调用。管理如何转换实际参数的规则与编译器是否在调用前遇到函数的原型(或者函数的完整定义)有关。
- **编译器在调用前遇到原型。**就像使用赋值一样,每个实际参数的值被隐式地转换成相应形式参数的类型。例如,如果把int类型的实际参数传递给期望得到double类型数据的函数,那么实际参数会被自动转换成double类型。
- 编译器在调用前没有遇到原型。编译器执行默认的实际参数提升:(1)把float类型的实际参数转换成double类型,(2)执行整值提升,即把char类型和short类型的实际参数转换成int类型。(C99实现了整数提升。)
默认的实际参数提升可能无法产生期望的结果。思考下面的例子:
#include <stdio.h>
int main(void){
double x = 3.0;
printf("Square: %d\n", square(x));
return 0;
}
int square(int n){
return n * n;
}
在调用square函数时,编译器没有遇到原型,所以它不知道square函数期望有int类型的实际参数。因此,编译器在变量x上执行了没有效果的默认的实际参数提升。因为square函数期望有int类型的实际参数,但是却获得了double类型值,所以square函数将产生无效的结果。通过把square的实际参数强制转换为正确的类型,可以解决这个问题:
printf("Square: %d\n", square((int) x));
当然,更好的解决方案是在调用square前提供该函数的原型。在C99中,调用square之前不提供声明或定义是错误的。
9.4 return语句
非void的函数必须使用return语句来指定将要返回的值。return语句有如下格式:
[return语句] return 表达式;
表达式经常只是常量或变量:
return 0;
return status;
但也可能是更加复杂的表达式。例如,在return语句的表达式中看到条件运算符(➤5.2节)是很平常的:
return n >= 0 ? n : 0;
执行这条语句时,表达式n >= 0 ? n : 0先被求值。如果n不是负值,这条语句返回n的值,否则返回0。
如果return语句中表达式的类型和函数的返回类型不匹配,那么系统将会把表达式的类型隐式转换成返回类型。例如,如果声明函数返回int类型值,但是return语句包含double类型表达式,那么系统将会把表达式的值转换成int类型。
如果没有给出表达式,return语句可以出现在返回类型为void的函数中:
return; /* return in a void function */
如果把表达式放置在上述这种return语句中将会获得一个编译时错误。下面的例子中,在给出负的实际参数时,return语句会导致函数立刻返回:
void print_int(int i){
if (i < 0)
return;
printf("%d", i);
}
如果i小于0,print_int将直接返回,而不会调用printf。return语句可以出现在void函数的末尾:
void print_pun(void){
printf("To C, or not to C: that is the question.\n");
return; /* OK, but not needed */
}
但是,return语句不是必需的,因为在执行完最后一条语句后函数将自动返回。
如果非void函数到达了函数体的末尾(也就是说没有执行return语句),那么如果程序试图使用函数的返回值,其行为是未定义的。有些编译器会在发现非void函数可能到达函数体末尾时产生诸如“control reaches end of non-void function”这样的警告消息。
9.5程序终止
既然main是函数,那么它必须有返回类型。正常情况下,main函数的返回类型是int类型,因此我们目前见到的main函数都是这样定义的:
int main(void){
...
}
以往的C程序常常省略main的返回类型,这其实是利用了返回类型默认为int类型的传统:
main(){
...
}
省略函数的返回类型在C99中是不合法的,所以最好不要这样做。省略main函数参数列表中的void是合法的,但是(从编程风格的角度看)最好显式地表明main函数没有参数。(后面将看到,main函数有时是有两个参数的,通常名为argc和argv,➤13.7节。)
main函数返回的值是状态码,在某些操作系统中程序终止时可以检测到状态码。如果程序正常终止,main函数应该返回0;为了表示异常终止,main函数应该返回非0的值。(实际上,这一返回值也可以用于其他目的。)即使不打算使用状态码,确保每个C程序都返回状态码也是一个很好的实践,因为以后运行程序的人可能需要测试状态码。
exit 函数
在main函数中执行return语句是终止程序的一种方法,另一种方法是调用exit函数,此函数属于<stdlib.h>头(➤26.2节)。传递给exit函数的实际参数和main函数的返回值具有相同的含义:两者都说明程序终止时的状态。为了表示正常终止,传递0:
exit(0); /* normal termination */
因为0有点模糊,所以C语言允许用EXIT_SUCCESS来代替(效果是相同的):
exit(EXIT_SUCCESS); /* normal termination */
传递EXIT_FAILURE表示异常终止:
exit(EXIT_FAILURE); /* abnormal termination */
EXIT_SUCCESS和EXIT_FAILURE都是定义在<stdlib.h>中的宏。EXIT_SUCCESS和EXIT_FAILURE的值都是由实现定义的,通常分别是0和1。
作为终止程序的方法,return语句和exit函数关系紧密。事实上,main函数中的语句
return 表达式;
等价于
exit(表达式);
return语句和exit函数之间的差异是:不管哪个函数调用exit函数都会导致程序终止,return语句仅当由main函数调用时才会导致程序终止。一些程序员只使用exit函数,以便更容易定位程序中的全部退出点。
9.6递归
如果函数调用它本身,那么此函数就是递归的(recursive)。例如,利用公式,下面的函数可以递归地计算出!的结果:
int fact(int n){
if (n <= 1)
return 1;
else
return n * fact(n-1);
}
有些编程语言极度地依赖递归,而有些编程语言甚至不允许使用递归。C语言介于中间:它允许递归,但是大多数C程序员并不经常使用递归。
为了了解递归的工作原理,一起来跟踪下面这个语句的执行:
i = fact(3);
下面是实现过程:
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为止。
下面是递归的另一个示例:利用公式计算x的n次方的函数。
int power(int x, int n){
if (n == 0)
return 1;
else
return x * power(x, n - 1);
}
调用power(5, 3)将会按照如下方式执行:
power(5, 3)发现3不等于0,所以power(5, 3)调用
power(5, 2),此函数发现2不等于0,所以power(5, 2)调用
power(5, 1),此函数发现1不等于0,所以power(5, 1)调用
power(5, 0),此函数发现0是等于0,所以返回1,从而导致
power(5, 1)返回5×1=5,从而导致
power(5, 2)返回5×5=25,从而导致
power(5, 3)返回5×25=125。
顺便说一句,通过把条件表达式放入return语句中的方法可以精简power函数:
int power(int x, int n){
return n == 0 ? 1 : x * power(x, n - 1);
}
一旦被调用,fact函数和power函数都仔细地测试“终止条件”。调用fact函数时,它会立刻检查参数是否小于或等于1;调用power函数时,它先检查第二个参数是否等于0。为了防止无限递归,所有递归函数都需要某些类型的终止条件。
练习题:
- 编写一个函数,计算两个整数的和。
- 编写一个函数,比较两个整数的大小,返回较大的那个。
- 编写一个函数,交换两个整数的值。
- 编写一个函数,判断一个整数是否为奇数。
- 编写一个函数,计算一个整数的绝对值。
- 编写一个函数,计算一个整数的平方。
评论区