张银峰的编程课堂

函数入门

假如我们设计了一个购物清单小票打印程序,大概如下这个样子:

#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;
}

glimix.com

通过我们的卖力宣传,附近的友好商户也决定采购软件,但用户希望使用波浪线~作为分隔符,于是我们将代码复制一份进行改动。随着更多客户的接入,程序版本越来越多,而我们重复修改着大量类似的代码,在这个无趣的过程中,其中一些可变的细节引起了我们的注意:

  • 客户的名称不尽相同。
  • 客户对分隔线符号有各自的喜好。
  • 客户使用不同的打印设备,所用纸张规格也不一样,继而分隔线的长度也不一样。

显然,如果有一种方法,能将各个版本间这些类同的问题统一化,并依据外部的数据提供差异化表达,那相当于我们仅用一份代码就可以服务不同用户,这就是函数的用武之地。

定义函数

一个函数是用于完成特定任务的程序代码的自包含单元,它可以执行某些动作(如向屏幕输出、控制外部设备),或执行运算产生结果,函数定义的语法如下:

返回值类型 函数名(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;
}

ZYF

使用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;
}

ZYF

改进清单程序

现在回到购物清单的程序上,客户想使用不同的字符作为分隔线,针对这点,我们有以下几点思考:

  • 我们需要一个名为“打印分隔线”的函数。
  • 函数应该返回什么?这里只是简单的输出符号线,所以不需要返回值。
  • 函数需要哪些参数?客户需要不同的字符来展现分隔线,这可以用一个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;
}

glimix.com

两次调用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;
}

glimix.com

可以看到,函数给了我们将一段代码组织成独立"模块"的能力,有些“模块”甚至可以多次复用。通过将程序划分为清晰合理的函数,使得代码编写与维护更加容易。函数由函数名标记,因此也可以说函数就是一个已命名的代码块。最后,我们将main函数中的逻辑组织到另一个函数中,虽然这不是必须的,但确实可以这么做。

void run_app()
{
    print_name();
    print_line('-', 60);
    print_list();
    print_line('=', 60);
    print_price();
}

int main()
{
    run_app();
    return 0;
}

练习

1 编写函数输出某一区间整数的立方值

2 编写函数判断指定年份是否为闰年