函数入门
假如我们设计了一个购物清单小票打印程序,大概如下这个样子:
#include <stdio.h>
int main()
{
printf("\t\t 大王超市 \t\t\n");
printf("=======================================\n");
printf("苹果 2.0Kg 16.00\n");
printf("山竹 1.8Kg 32.24\n");
printf("...\n");
printf("=======================================\n");
printf("总计 128.64\n");
return 0;
}

通过我们的卖力宣传,附近的友好商户也决定采购软件,但用户希望使用波浪线~作为分隔符,于是我们将代码复制一份进行改动。随着更多客户的接入,程序版本越来越多,而我们重复修改着大量类似的代码,在这个无趣的过程中,其中一些可变的细节引起了我们的注意:
- 客户的名称不尽相同。
- 客户对分隔线符号有各自的喜好。
- 客户使用不同的打印设备,所用纸张规格也不一样,继而分隔线的长度也不一样。
显然,如果有一种方法,能将各个版本间这些类同的问题统一化,并依据外部的数据提供差异化表达,那相当于我们仅用一份代码就可以服务不同用户,这就是函数的用武之地。
定义函数
一个函数是用于完成特定任务的程序代码的自包含单元,它可以执行某些动作(如向屏幕输出、控制外部设备),或执行运算产生结果,函数定义的语法如下:
返回值类型 函数名(0个或多个以逗号分隔的参数声明列表)
语句
我们可以从生活的角度去类比函数语法,比如:在自动售货机上买了一瓶饮料,投币10元,售货机弹出饮料并找零5元,这个行为中包含下面这些元素:
- 事件:买饮料;这相当于对行为的总结, 函数名与之类似,用于描述语句所要实现的目标。
- 货币:参与事件的元素,相当于函数的参数。
- 饮料:事件的回报,相当于函数的返回值
- 找零:事件的状态反馈,也可以认为是返回值,但实际的函数只能有一个返回值。
一个简单的计算矩形面积的函数或许更能说明问题;
- 任务:计算矩形面积;即函数的名称:calc_rect_area
- 参与者:宽度与高度值;即函数的参数:width、height
- 过程:面积求值的实现;即函数的实现语句:width * height
- 反馈:计算的面积;即函数的返回值:area
假设计算建立在int型之上,这个过程可以用下面的C代码来表达。
int // 返回的面积是个整数
calc_rect_area // 函数名称用于准确的描述功能
(int width, int height) // 计算面积需要的参数
{
int area = width * height; // 函数实现
return area; // 使用return关键字返回值
}
// 移除注释之后的整个函数
int calc_rect_area(int width, int height)
{
int area = width * height;
return area;
}
使用void关键字可以指示函数不返回任何值,此时void不可省略。
void print_message(int times)
{
for (int i = 0; i < times; i++)
printf("Hello, world!\n");
}
如果函数不需要参数,可以使用void代表无参数列表,此时void可以省略。
void print_author(void)
{
printf("glimix.com");
}
// 等价于上面的定义
void print_author()
{
printf("glimix.com");
}
调用函数
函数被定义后并不能自动进行运算。我们需要显式的使用它才能生效,这被称为函数调用。语法如下:
函数名 (以逗号分隔的参数列表);
对于函数 calc_rect_area 可以这样调用:
- 首先是函数名:calc_rect_area
- 然后是左括号:calc_rect_area(
- 接下来传入需要的参数:calc_rect_area(24, 35
- 最后是右括号与语句结束符分号:calc_rect_area(24, 35);
函数调用完成后返回一个int值,因此我们可以在赋值语句中直接调用函数,以保存计算结果,如:
int area = calc_rect_area(33, 22);
也可以在其它函数需要的地方调用,如printf函数中调用以直接查看结果。
printf("area: %d\n", calc_rect_area(29, 28));
甚至,直接调用而丢弃这个结果,如:
calc_rect_area(99, 99);
C语言要求一个名称在使用之前必须是可见的(即已声时或定义),否则会产生错误,函数名也不例外。因此为了在main()函数中使用自己定义的函数,我们需要将函数calc_rect_area定义在main之前。
#include <stdio.h>
int calc_rect_area(int width, int height)
{
int area = width * height;
return area;
}
int main()
{
// 调用函数并用返回值初始化变量
int area = calc_rect_area(34, 54);
// 调用函数并将返回值作为其它函数的参数
printf("area: %d\n", calc_rect_area(32, 32));
// 调用函数并丢掉返回值
calc_rect_area(99, 67);
return 0;
}
改进清单程序
现在回到购物清单的程序上,客户想使用不同的字符作为分隔线,针对这点,我们有以下几点思考:
- 我们需要一个名为“打印分隔线”的函数。
- 函数应该返回什么?这里只是简单的输出符号线,所以不需要返回值。
- 函数需要哪些参数?客户需要不同的字符来展现分隔线,这可以用一个char参数来表达。
- 清单所用纸张大小可能不一样,我们还需要使用一个int参数来传递纸张宽度。
这样,构思下的第一版改进程序如下:
#include <stdio.h>
// 打印分隔线
void print_line(char c, int width)
{
for (int i = 0; i < width; i++)
printf("%c", c);
printf("\n");
}
int main()
{
printf("\t\t 大王超市 \t\t\n");
print_line('_', 50);
printf("苹果 2.0Kg 16.00\n");
printf("山竹 1.8Kg 32.24\n");
print_line('~', 50);
printf("总计 48.24\n");
return 0;
}

两次调用print_line函数时,我们传递了不同的参数,实现程序的定制操作。
继续改进,我们可以试着用函数把程序组织起来。比如,提供一个打印名称的函数;一个打印清单与总计的函数等。
#include <stdio.h>
// 打印超市名称
void print_name(void)
{
printf("\t\t Fruits Cube\n");
}
// 打印清单分隔线
void print_line(char c, int width)
{
for (int i = 0; i < width; i++)
printf("%c", c);
printf("\n");
}
// 打印清单
// 返回已经被打印的商品计数
int print_list()
{
printf("Apple 2.0Kg 16.00\n");
printf("Orange 6.8Kg 32.24\n");
return 2;
}
// 计算商品价格的函数
double calc_total_price()
{
// 调用另一个函数取得所购商品列表
// 通过循环调用每一个商品的价格计算方法
// 累加得到金额总计
return 128.64;
}
// 打印商品总金额
void print_price(void)
{
// 一个函数的返回值作为另一个函数的参数
printf("Total\t\t\t\t%f\n", calc_total_price());
}
int main()
{
print_name();
print_line('-', 60);
print_list();
print_line('=', 60);
print_price();
return 0;
}

可以看到,函数给了我们将一段代码组织成独立"模块"的能力,有些“模块”甚至可以多次复用。通过将程序划分为清晰合理的函数,使得代码编写与维护更加容易。函数由函数名标记,因此也可以说函数就是一个已命名的代码块。最后,我们将main函数中的逻辑组织到另一个函数中,虽然这不是必须的,但确实可以这么做。
void run_app()
{
print_name();
print_line('-', 60);
print_list();
print_line('=', 60);
print_price();
}
int main()
{
run_app();
return 0;
}
练习
1 编写函数输出某一区间整数的立方值
2 编写函数判断指定年份是否为闰年