英文原版:P183
在C语言中,什么是函数?
- 函数是一连串被组合到一起、有名字的语句。
- 每个函数本质上是一个小程序,自带声明和语句。
- 在C语言中,一个函数不一定会有参数,也不一定会计算一个值。
在C语言中,函数有什么作用?
- 函数是构建C程序的基础。
- 使用函数可以将一个程序划分成更易于理解和修改的小块。
- 使用函数可以避免编写被多次使用的代码,从而消除编程中的一些乏味。
- 函数是可以复用的:取来自程序A中的函数,在程序B中使用。
本章的主要内容有:
- 9.1节介绍如何定义和调用函数。
- 9.2节介绍函数声明,以及函数声明跟函数定义的区别。
- 9.3节介绍如何给函数传递参数。
- 9.4节介绍return语句。
- 9.5节介绍跟程序终止相关的问题。
- 9.6节介绍递归。
9.1节 函数定义和函数调用
函数定义
返回值类型 函数名(形式参数)
{
声明
语句
}
针对返回值类型有如下几条规则:
- 函数的返回值类型不能是数组;
- 如果返回值类型是void,则表明该函数没有返回值;
- 在C89中,如果一个函数省略了返回值类型,则假设函数的返回值类型为int;在C99中,省略函数的返回值类型是非法的。
针对形式参数有几点说明:
- 每个形式参数的前面是参数类型;
- 形式参数间用逗号分隔;
- 如果函数没有任何参数,则括号里应该有void;
- 即使多个形式参数有相同的类型,也必须对每个参数进行单独的声明。
针对函数体有几点说明:
- 函数体可以包含声明和语句;
- 函数体内声明的变量专属于此函数,不能被其他函数修改或者检查;
- 对于返回值是void类型的函数,函数体可为空;
例1 错误的函数定义
double average(double a, b){
}
例2 有返回值且有形式参数的函数定义
double average(double a, double b){
return (a+b)/2;
}
例3 无返回值有形式参数的函数定义
void print_count(int n) {
printf("T minus %d and counting\n", n);
}
例4 无返回值无形式参数的函数定义
void print_pun(void) {
printf("To C, or not to C: that is the question.\n");
}
函数调用
格式:
函数名(参数列表)
例1 函数调用示例
average(x, y)
print_count(i)
print_pun()
例2 返回值是void的函数调用语句
print_count(i);
print_pun();
例3 保存一个非void函数的调用语句
avg = average(x, y);
if (average(x, y) > 0) {
printf("Average is positive\n");
}
printf("The average is %g\n", average(x, y));
例4 丢弃非void函数返回值的调用语句
average(x,y);
程序示例1:计算平均值
源文件:average.c
#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", z, x, average(z, x));
return 0;
}
程序示例2:输出递减计数器
源文件:countdown.c
#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;
}
程序示例3:打印双关语
源文件pun2.c
#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;
}
程序示例4:检测一个数是不是素数
源文件:prime.c
#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;
}
9.2节 函数声明
函数声明格式:
返回值类型 函数名(形式参数列表);
规则:
- 在函数调用前必须进行声明。
- 函数声明必须跟函数定义保持一致。
注:
这种函数声明就是函数原型。
函数原型(函数声明)有什么作用?
- 告诉编译器这是一个函数的简介,该函数的定义会在稍后给出;
- 描述如何调用一个函数:要提供多少个实际参数,这些实际参数的类型是什么,返回值是什么类型等;
- 函数原型不一定要列出形式参数的名字,但一般不建议这么做;
例1 计算平均数
average2.c
#include <stdio.h>
double average(double a, double b);
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", z, x, average(z, x));
return 0;
}
double average(double a, double b){
return (a+b)/2;
}
9.3节 实际参数
区分形参和实参
- 形参出现在函数定义里;
- 实参出现在函数调用里;
在C语言中,形参有什么性质?
形参跟变量类似,形参的初始值是相匹配的实参的值。
在C语言中,实参有什么性质?
按值传递:当函数被调用时,会对每个参数求值,并把每个参数的值拷贝给相应的形参。
由于形参包含的是实参值的拷贝值,所以在程序执行过程中对形参的修改,不会影响实际参数。
按值传递有什么优势?
- 减少真正需要的变量数量
例1 利用按值传递的性质来减少真正需要的变量数量
源程序:
int power(int x, int n)
{
int i, result = 1;
for(i=1; i<=n; i++){
result = result * x;
}
return result;
}
减少变量后:
int power(int x, int n)
{
int result = 1;
while(n--){
result = result * x;
}
return result;
}
按值传递有什么缺点?
- 无法编写某些类型的函数,比如现在需要一个能将double类型值分解成整数部分和小数部分。由于一个函数不能返回两个值,则尝试使用如下方式来实现:
void decompose(double x, long int_part, double frac_part)
{
int_part = (long)x;
frac_part = x - int_part;
}
但是由于按值传递的性质,不起作用。此时,需要利用指针的性质来实现,修改函数的参数类型为:
void decompose(double x, long *int_part, double *frac_part)
{
*int_part = (long)x;
*frac_part = x - *int_part;
}
实参的类型转换规则
C语言允许函数调用时的实参的类型跟形参的类型不匹配。
转换规则:
- 如果编译器在函数调用前碰到过函数原型,则实参的值就隐式地转换成形参的值,比如
int
实参值传递给double
形参,则会自动将int转换成double; - 否则,编译器会执行默认的类型提升规则:float实参值转换成double;char类型实参值和short类型实参值转换成int;
数组作为函数参数
一维数组作为函数参数的约定:
- 需要额外提供数组的长度也作为函数参数。
- 函数是没有办法来检查是否传递了正确的数组长度,所以务必要保证传递的数组长度值要小于等于实际值;如果超过实际值,会出现不确定的行为。
- 函数是允许修改数组参数元素的值的,且该修改会反映在相应实参数组中。
例1 1维数组作为函数参数
函数声明:
int sum_array(int a[], int n);
函数定义:
int sum_array(int a[], n)
{
int i, sum = 0;
for(i=0; i<n; i++){
sum += a[i];
}
}
函数调用
#define LEN 100
int main(void)
{
int b[LEN], total
...
total = sum_array(b, LEN);//注意,这里不能写成sum_array(b[], LEN)
...
}
例2 修改数组参数的元素值,会修改对应实参数组的元素值
函数定义:
void store_zeros(int a[], int n)
{
int i;
for (i=0; i< n; i++) {
a[i] = 0;
}
}
根据上述函数定义,可知:函数调用store_zeros(b, 100)
的效果就是在数组b的前100个元素里存储0值。
多维数组作为函数参数相关规则:
- 当参数声明时,只有一维数组的长度可以省略,其余维的长度必须是确定的。
- 由于前述声明的约束,使该声明方法,我们是不能传递具有任意列数的多维数组的;一般来说有两种解决办法:变长数组作为数组参数,或者指针数组作为参数等。
例3 多维数组作为函数参数
#define LEN 10
int sum_dimensional_array(int a[][LEN], int n)
{
int i, j, sum = 0;
for (i=0; i<n;i++){
for(j=0;j<LEN;j++){
sum += a[i][j];
}
}
}
可变长度数组作为函数参数
- 一维可变数组形参:通过指定数组参数的长度使得函数的声明和定义更具体。
- 多维可变数组形参:可传递任意列数。
例1 可变长度数组作为函数参数
函数原型
int sum_array(int n, int a[n]);
int sum_array(int n, int a[*]);
函数定义
//注意:对于可变数组来说,这里的n跟a[n]的顺序是不能交换的
int sum_array(int n, int a[n])
{
...
}
例2 使用多维可变数组形参来可传递任意列数
函数声明:
int sum_dimensional_array(int n, int m, int a[n][m]);
函数定义
int sum_dimensional_array(int n, int m, int a[n][m])
{
int i, j, sum = 0;
for (i=0; i<n;i++){
for(j=0;j<m;j++){
sum += a[i][j];
}
}
}
在实参中使用复合字面量
什么是复合字面量?
一个没有名字的数组,它的元素需要简单地列出。
格式:
(类型 []){实参列表}
例1 使用复合字面量来进行函数调用
//(int [5]){3, 0, 3, 4, 1}
total = sum_array((int [5]){3, 0, 3, 4, 1}, 5);
//在复合字面量里,可不用指出数组长度:(int []){3, 0, 3, 4, 1}
total = sum_array((int []){3, 0, 3, 4, 1}, 5);
//复合字面量可包含指定初始化式:(int [10]){8, 6}
total = sum_array((int [10]){8, 6}, 10);
//复合字面量可包含任意表达式:(int []){2*i, i + j, j * k}
total = sum_array((int []){2*i, i + j, j * k}, 3);
//只读复合字面量:(const int []){5, 4}
total = sum_array((const int []){5, 4}, 2);
在数组参数的声明中使用static
例1 使用static来声明一维数组
int sum_array(int a[static 3], int n)
{
...
}
解释:
-
int a[static 3]
表明数组a的长度至少是3; - 这里static对程序的行为没有任何影响;
- 这里使用static的效果是允许C编译器来更快速地生成访问数组的指令;
注:
如果一个数组参数有多维,则static只能使用在第一个维。
9.4节 return语句
格式:
return 表达式;
解释:
- 表达式通常是常数或者变量,也可以是复杂的表达式。
- 当return语句里的表达式的值跟函数定义的返回值类型不匹配时,则表达式的值会被隐式地转换成函数定义的返回值类型。
- 如果一个非void函数执行到函数体的结尾处也没有遇到return语句,而某个程序又要使用该函数的返回值,则该程序的行为是没有定义的。
例1 常见的返回语句
return 0;
return status;
return n>=0 ? n:0;
例2 在void函数里的返回语句
return ;
9.5节 程序终止
有两种方式来终止一个C程序:
- return语句;
- 调用包含在头文件
<stdlib.h>
中的exit
函数;
return语句退出程序
通过main函数里的return语句来终止程序。
main函数必须有返回值,且返回值类型为int。
没有返回值的main函数是非法的。
main函数的返回值是一个状态码,可用该状态码来测试程序何时终止。
- 如果程序正常终止,则main函数应该返回0;
- 如果main函数返回非0值,则表明程序非正常退出;
注:
main函数有参数吗?
- main函数有时会有两个参数,比如argc、argv等;
- 如果main函数没有参数,也要显示地表明;
例1 正常退出程序
//如果main函数没有参数,也要显式地表明
int main(void)
{
...
return 0;
}
exit函数终止程序
例1 正常退出程序
exit(0);//或者exit(EXIT_SUCCESS);
例2 非正常退出程序
exit(EXIT_FAILURE);
return语句退出程序跟exit语句之间的关系
相同点:
return 表达式;
等价于
exit(表达式);
不同之处:
- 无论哪个函数调用exit,都会导致程序终止;
- 只有在main函数里出现return语句,才会导致程序终止;
9.6节 递归
C语言允许递归,但不经常使用递归。
为了避免无限递归,所有的递归都必须有中止条件。
如果一个函数自己调用自己,则称这个函数是递归的。
例1 计算一个数的阶乘
int fact(int n)
{
if (n <= 1)
{
return 1;
}
else
{
return n * frac(n-1);
}
}
例2 计算一个数的幂次方
int power(int x, int n)
{
if (n == 0)
{
return 1;
}
else
{
return x * power(x, n-1);
}
}
程序示例:快速排序
qsort.c
#include <stdio.h>
#define N 10
void quicksort(int a[], int low, int high);
int split(int a[], int low, int high);
int main(void)
{
int a[N], i;
printf("Enter %d numbers to be sorted: ", N);
for(i=0;i<N;i++)
{
scanf("%d", &a[i]);
}
quicksort(a, 0, N-1);
printf("In sorted order: ");
for (i = 0; i < N; ++i)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
void quicksort(int a[], int low, int high)
{
int middle;
if (low >= high)
{
return ;
}
middle = split(a, low, high);
quicksort(a, low, middle -1);
quicksort(a, middle+1, high);
}
int split(int a[], int low, int high)
{
int part_element = a[low];
for(;;)
{
// 从右边开始向左扫描,找第一个被part_ment小的元素
// 将其放置到part_ment的左边
while(low < high && part_element <= a[high])
{
high--;
}
if(low >= high)
{
break;
}
a[low++] = a[high];
// 从左边开始向右边扫描,找第一个被part_ment大的元素
// 将其放置到part_ment的右边
while(low < high && a[low]<=part_element)
{
low++;
}
if (low >= high)
{
break;
}
a[high--]=a[low];
}
//将part_ment放置到适当的位置
a[high] = part_element;
return high;
}