-- 简书作者 谢恩铭 转载请注明出处

第一部分第九课:函数


上一课是C语言探索之旅 | 第一部分第八课:第一个C语言小游戏

这一课我们将会用函数这个重中之重来结束《C语言探索之旅》的第一部分(基础部分),而第二部分将要迎接我们的就是C语言的高级技术了。

第二部分会比较难哦,不过不用担心,我们一点点学习。只要方向对,肯花时间,C语言一点也不可怕。

这一课里我们也会给大家讲C语言程序所基于的原则。

我们将要学习如何将程序分块管理,有点像乐高积木。

其实所有C语言的大型程序都是小程序块的集合,而这些小程序块我们称之为函数。

在面向对象的语言(如Java,C++)里面,函数又被称为方法,当然这里我们只讨论C语言(面向过程的语言),不讨论面向对象的语言,sorry,我又废话了... 好吧,我们只面向C语言这个对象。

函数的创建和调用


在之前的课程中我们已经学过:所有的C语言程序都是由main函数开始运行的。那时候我们也展示了一个概要图,里面有一些术语:

最上面的部分我们称之为“预处理指令”,很容易辨识,因为以#号开头,而且通常总是放在程序的最前面。

下面的部分就是我们要学习的函数了,这里的示例是main函数。

前面说过,C语言的程度都是以main函数为入口函数的。

一个C程序要运行,必须要有main函数。只不过,目前为止我们写的所有程序,包括上一课的小游戏,也只是在main函数里面捣鼓而已,我们还没跳出过main函数过。

那你要问了:这样不好吗?

答案是:

并不是说这样不好,但这并不是C程序员平时所做的。

几乎没有程序员会只在main函数的大括号内部写代码。

到目前为止我们所写的程序都还比较短小,但是想象一下如果程序变得很大,代码几千几万甚至上百万行,难道我们还把这些代码都塞在main函数里面吗?

所以我们现在来学习如何更好地规划我们的程序。

我们要学习将程序分成很多小块,就像乐高积木的每一个小块一样,这些小块搭起来却可以组成很多好玩的形状。

这些程序小块我们称其为函数。

一个函数会执行某些操作,并返回一个值。程序就是一个代码序列,负责完成特定的任务。

我们说一个函数有输入和输出,如下图所示:

可以把函数想象成一台制作香肠的机器,在输入那一头你把猪装进去,输出那一头就出来香肠了。这酸爽,不言而喻~

函数就像香肠制造机

当我们在程序中调用一个函数的时候,会依次发生三个步骤:

  1. 输入:给函数传入一些信息(借着给函数一些参数)

  2. 运算:因着输入里传进去的信息,函数就可以完成特定任务了

  3. 输出:做完运算后,函数会返回一个结果。我们称其为输出或者返回值

举个实际例子,比如我们有个函数叫做multipleTwo,作用是将输入乘以二,如下所示:

函数的目的是为了让源代码更加结构分明,也节省源代码数目,因为我们就不用每次都输入重复的代码片段而只需要调用函数就好了。

再设想一下:

之后我们可能会想要创建一个叫showWindow(显示窗口)的函数,作用是在屏幕上显示一个窗口。

一旦函数写好之后(当然写的过程是最难的),我们就只需要说:“那个谁,给我去打开一个窗口”,showWindow函数就会为我们在屏幕上显示一个窗口。

我们也可以写一个displayPersonage的函数,作用是为我们在屏幕上显示一个游戏人物。

函数的构成


我们在之前的课中已经接触过函数了,就是非常重要的main函数。然而还是需要我们来介绍一下一个函数的构成到底是怎么样的。
下面是函数的语义学的结构,这是一个需要了解的模板:

类型 函数名(参数)
{
// 函数体,在这里插入指令
}

关于这个模板我们需要掌握四点:

  1. 函数类型:对应输出类型,也可以把其看做函数的类型。和变量类似,函数也有类型,这类型取决于函数返回值的类型。如果一个函数返回一个浮点数(带小数点的),那么自然我们会将函数类型定为float或者double;如果返回整数,那么我们一般会将类型定为int或long。但是我们也可以创建不返回任何值的函数。

  2. 函数名:这是你的函数的名字。你可以给你的函数起任意名字,只要遵从给变量命名的相同的规则就好。

  3. 函数的参数(对应输入):参数位于函数名之后的圆括号内。这些参数是函数要用来做操作(运算)的数据。你可以给函数传入任意数量的参数,也可以不传入任何参数。

  4. 函数体:大括号规定了函数的起始和结束范围。在大括号中你可以写入任意多的指令。对于上面的multipleTwo函数,需要写入将输入的数字乘以2的相关操作指令。

根据函数类型,函数可以分为两类:

  1. 返回一个值的函数,这样的函数,我们将其类型定为对应的值的类型(char,int,long,double等)

  2. 不返回任何值的函数,这样的函数,我们将其类型定为void(void在英语中是“空的,无效的”之意)

创建函数


我们不要再迟延了,马上给出一个实例。用的还是我们上面提过的multipleTwo这个函数:

这个函数的输入是一个整型int,输出也是int类型的数。

int multipleTwo(int number)
{
  int result = 0;
  result = 2 * number; // 我们将提供的数乘以2
  return result; // 我们将2倍的数返回
}

这就是你的第一个除了main以外的函数,自豪不?

return result;

这句话一般放在函数体的最后,用于返回一个值。这句话意味着:“函数你给我停下,然后返回这个值”。这里的result必须是int类型的,因为函数类型是int,所以返回值也必须是int类型。

result这个变量是在multipleTwo函数中声明/创建的,所以它只能在这个函数里面用,不能在另一个函数比如main中使用,所以是multipleTwo函数的私有变量。

但上面的代码是不是最简单的呢?

不是,还可以简化,如下:

int multipleTwo(int number)
{
  return 2 * number;
}

上面的代码做的是一样的事情,写起来也更简单,函数体内只有一句话。

通常来说,我们写的函数都会有多个变量,以便做运算,multipleTwo这个函数算是相当简单了。

多个参数,或没有参数


多个参数

我们的multipleTwo函数只有一个参数,但是我们也可以创建有几个参数的函数,比如下面这个加法函数addition:

int addition(int a, int b)
{
  return a + b;
}

可以看到,只需要用一个逗号来分隔参数就好了。

没有参数

有些函数,虽然不太常见,可能会没有参数。例如一个用来显示Hello(你好)的函数:

void hello()
{
  printf("Hello");
}

如上所见,这个函数没有任何参数,此外,可以看到我们还把函数类型定为了void,就是空,所以也没有return语句用于返回一个值,所以这个函数也没有返回值。彻头彻尾的黑五类…

调用函数


现在我们来看一个程序,复习一下我们之前学的内容。

我们要用到我们的multipleTwo函数,来计算一个数的两倍的值。

我们暂时把multipleTwo函数写在main函数之前,如果放在main函数之后会出错,以后的课程我们会解释为什么。

#include <stdio.h>

int multipleTwo(int number)
{
  return 2 * number;
}

int main(int argc, char *argv[])
{
  int initial = 0, twice = 0;

  printf("请输入一个整数... ");
  scanf("%d", &initial);

  twice = multipleTwo(initial);
  printf("这个数的两倍是 %d\n", twice);

  return 0;
}

我们的程序是从main函数开始运行的,这个大家已经知道了。

我们首先请求用户输入一个整数,将其值传递给multipleTwo函数,并且把multipleTwo函数的返回值赋给twice这个变量。

仔细看下面这一行,这是我们最关心的一行代码,因为正是这一行调用了我们的multipleTwo函数。

twice = multipleTwo(initial);

在括号里,我们将变量initial当做输入传递给函数,也正是这个变量,函数将要用于其内部的处理。

这个函数返回一个值,这个值我们赋给twice这个变量。

其实这一行中,我们就是命令电脑:“让multipleTwo函数给我计算initial的两倍的值,并且将结果储存到twice这个变量中”。

详细的分步解释


也许对于初学者,理解起来还是有些许困难。

不用担心,我相信通过下面的分步解释,大家会明白得更透彻。

这个特殊注释的代码向大家展示了程序的运行顺序:

#include <stdio.h>

int multipleTwo(int number) // 6
{
  return 2 * number; // 7
}

int main(int argc, char *argv[]) // 1
{
  int initial = 0, twice = 0; // 2

  printf("请输入一个整数... "); // 3
  scanf("%d", &initial); // 4

  twice = multipleTwo(initial); // 5
  printf("这个数的两倍是 %d\n", twice); // 8

  return 0; // 9
}

上面的编号表示了执行的顺序:

1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 -> 8 -> 9

  1. 程序从main函数开始执行

  2. 在main函数中的命令一行一行地被执行

  3. 执行printf输出

  4. 执行scanf读入数据,赋值给变量initial

  5. 读入指令... 啊,调用multipleTwo函数了,因此程序跳到上面的multipleTwo函数中去执行

  6. 我们运行multipleTwo函数,并接受一个数作为输入(number)

  7. 我们对number做运算,并且结束multipleTwo函数,return意味着函数的结束,并且返回一个值

  8. 我们重新回到main函数的下一条指令,用printf输出

  9. 又一个return,这次是main函数的结束,于是整个程序运行完毕。

变量initial被传值给multipleTwo的参数number(另一个变量),称为值传递。

当然其实原理是做了一份变量initial的拷贝,把拷贝赋值给了number,这个值传递的概念以后学习指针那一章会再详述。

这里如果我们把initial改名为number也是可以的,并不会与函数multipleTwo的参数number冲突。因为参数number是属于multipleTwo这个函数的专属变量。

测试程序


下面是程序运行起来的一个实例:

请输入一个整数... 10
这个数的两倍是 20

当然你不必将multipleTwo函数的返回值赋给一个变量,也可以直接将multipleTwo函数的返回值传递给另一个函数,就好像multipleTwo(intial)是一个变量一般。

仔细看下面这个程序,跟上面几乎是一样的,但是修改了最后一个printf的行为,我们也没有使用twice这个变量,因为不必要:

#include <stdio.h>

int multipleTwo(int number)
{
  return 2 * number;
}

int main(int argc, char *argv[])
{
  int initial = 0, twice = 0;

  printf("请输入一个整数... ");
  scanf("%d", &initial);

  // 函数的结果(返回值)直接传递给printf函数,而没有通过第三方变量
  printf("这个数的两倍是 %d\n", multipleTwo(initial));

  return 0;
}

我们可以看到,这次的程序直接将multipleTwo函数的返回值传递给了printf函数。

当程序运行到这一行会发生什么呢?

很简单,电脑看到这一行是printf函数,所以调用标准输入输出库的printf函数,向printf函数传递我们给的所有参数。

第一个参数是要显示的语句,第二个参数是一个整数。

电脑又知道要把这个整数值传递给printf函数,必须先调用multipleTwo函数,所以它就乖乖地去调用multipleTwo函数,做两倍乘法运算,并且直接把结果传递给printf函数。

这就是函数的层叠式调用,这样做的好处是,一个函数可以按需调用另一个函数。

只要愿意,我们的multipleTwo函数也可以再调用其他的函数,只要你肯写,然后这个函数再调用其它函数,依次类推。

这就是C语言程序所基于的原则。所有的代码都是有规划地组合在一起的,类似乐高积木。

最后,最艰难的当然是编写函数了,一旦完成,你就只需要调用它就好了,不需要太担心函数内部所做的运算。

使用函数可以大大降低代码的重复度,相信我,你会非常需要函数的。

一些函数的实例


如果一起学习过之前的课程,你应该会有这种印象:

我就是个“例子狂人”。

是的,因为我很喜欢用实例来加深理解。

因为我觉得理论虽好,但如果只有理论,那我们就不能很好地掌握知识,而且不知道怎么应用,那就很可惜了。想起了“劲酒虽好,可不要贪杯哦”那句广告词…

所以下面我们会一起看几个函数的实例,以便读者对函数有更深入的了解。我们尽量展示不同情况,使大家看到可能出现的各种函数类型。

欧元/人民币转换


我们来写一个函数,用于转换欧元到人民币。

查了一下最新的汇率:

1欧元=7.4694人民币元

#include <stdio.h>

double conversion(double euros)
{
  double rmb = 0;

  rmb = 7.4694 * euros;
  return rmb;
}

int main(int argc, char *argv[])
{
  printf("10 欧元 = %f 人民币\n", conversion(10));
  printf("50 欧元 = %f 人民币\n", conversion(50));
  printf("100 欧元 = %f 人民币\n", conversion(100));
  printf("200 欧元 = %f 人民币\n", conversion(200));

  return 0;
}

你也可以写一个人民币转换为欧元的小程序。

惩罚


接下来看一个函数,这个函数不会返回任何值,所以类型是void。这个函数会根据传入的参数在屏幕上显示一定次数的信息。

这个函数只有一个参数,那就是显示惩罚语句的次数:

#include <stdio.h>

void punish(int lineNumber)
{
  int i;

  for (i = 0 ; i < lineNumber ; i++)
  {
    printf("我不应该有钱任性\n");
  }
}

int main(int argc, char *argv[])
{
  punish(5);

  return 0;
}

显示结果如下:

我不应该有钱任性
我不应该有钱任性
我不应该有钱任性
我不应该有钱任性
我不应该有钱任性

矩形面积


矩形的面积很容易计算:长 x 宽。
我们来写一个求矩形面积的函数,它有两个参数:矩形的长和矩形的宽。返回值是矩形的面积:

#include <stdio.h>

double rectangleArea(double length, double width)
{
  return length * width;
}

int main(int argc, char *argv[])
{
  printf("长是10,宽是5的矩形面积是 %f\n", rectangleArea(10, 5));
  printf("长是3.5,宽是2.5的矩形面积是 %f\n", rectangleArea(3.5, 2.5));
  printf("长是9.7,宽是4.2的矩形面积是 %f\n", rectangleArea(9.7, 4.2));

  return 0;
}

显示结果:

长是10,宽是5的矩形面积是 50.000000
长是3.5,宽是2.5的矩形面积是 8.750000
长是9.7,宽是4.2的矩形面积是 40.740000

我们可以直接在函数里显示 长,宽和计算所得的面积吗?

当然可以。这样的情况下,函数就不必返回任何值了,函数计算出矩形面积,然后直接显示在屏幕上:

#include <stdio.h>

void rectangleArea(double length, double width)
{
  double area = 0;

  area = length * width;
  printf("长为 %f 宽为 %f 的矩形的面积是 %f\n", length, width, area);
}

int main(int argc, char *argv[])
{
  rectangleArea(10, 5);
  rectangleArea(3.5, 2.5);
  rectangleArea(9.7, 4.2);

return 0;
}

我们可以看到,printf函数在函数体内被调用,显示的结果和之前把printf放在main函数里是一样的。只不过我们用的方法不一样罢了。

菜单


还记得之前的课程中菜单的那个例子吗?(皇上,您还记得大明湖畔的夏雨荷么?)
这次我们用自定义的函数来重写一次,会更详细和优化:

#include <stdio.h>

int menu()
{
  int choice = 0;

  while (choice < 1 || choice > 4)
  {
    printf("菜单 :\n");
    printf("1 : 北京烤鸭\n");
    printf("2 : 麻婆豆腐\n");
    printf("3 : 鱼香肉丝\n");
    printf("4 : 剁椒鱼头\n");
    printf("您的选择是 ? ");
    scanf("%d", &choice);
  }

  return choice;
}

int main(int argc, char *argv[])
{
  switch (menu())
  {
    case 1:
      printf("您点了北京烤鸭\n");
      break;
    case 2:
      printf("您点了麻婆豆腐\n");
      break;
    case 3:
      printf("您点了鱼香肉丝\n");
      break;
    case 4:
      printf("您点了剁椒鱼头\n");
      break;
  }

  return 0;
}

这个程序还可以改进:

你可以在用户输入一个错误的数字时显示一个错误信息,而不是直接继续让其点单。

总结


  1. 函数之间可以互相调用,因此main函数可以调用C语言系统定义好的函数例如scanf和printf等,也可以调用我们自己定义的函数

  2. 一个函数接受一些变量作为输入,我们将其称为函数的参数(也有空(void)参数的函数)

  3. 函数会用这些参数来做一系列的操作,之后会用return返回一个值(也有无返回值的函数)

第一部分第十课预告:练习题+习作


今天的课就到这里,一起加油咯。

下一次我们学习第一部分第十课,来做一些帮助巩固知识点的练习题吧!

有答案,但是希望大家先做完再对答案。

C语言探索之旅 | 第一部分第十课:练习题+习作