
2.4.4 通过函数的参数传递数值
函数可以从调用方那里接受一些输入值,然后在函数体中使用这些值。我们在定义函数时,需要把调用方应该传入此函数(也就是此函数能够从调用方那里接受)的参数,并指定每个参数的类型,这样调用方在调用此函数时,就必须传入符合要求的参数。调用函数时所传入的参数个数以及每个参数的类型必须与我们在定义函数时所指定的个数及类型相符。换句话说,调用函数时所用的“签名”必须与我们定义的函数签名(即函数特征)相符。
前面我们已经遇到了这样一个带有参数的函数,也就是printf()函数,我们在调用该函数时是这样写的:printf("Hello, world!\n");。这意味着我们给它传入了一个参数,具体来说,就是一个值为"Hello, world!\n"的字符串。任何一个字符串几乎都可以传给printf()去打印,只要我们把它括在一对双引号里面就行。
定义函数的时候需要在函数名称右边的括号里面指定函数的参数,我们把这对括号以及里面所指定的参数合起来记为(...),省略号(也就是...)表示参数列表中的0个或多个参数,这些参数之间以逗号(,)分隔,逗号也是C语言里的一种标记,我们在前面的程序里还没有用到该标记。如果你不想给函数指定参数,那么可以写成(void),也可以简写为(),这两种写法都表示空白的参数列表[1]。
(参数列表中的)每个参数都由两部分组成,也就是数据类型与标识符。数据类型指这个参数是一个什么样的值,例如整数、小数、字符串,还是其他某种类型的值。标识符则指函数在需要访问该参数的取值时所使用的名称。多个参数之间用逗号隔开。笔者会在下一章详细讲解数据类型。参数的标识符跟函数的标识符很像。我们在程序里面调用某个函数时需要指出该函数的标识符(也就是该函数的名称),而这个函数如果要在它的函数体里面访问某个参数,那么也需要指出这个参数的标识符(也就是这个参数的名称)。下面我们来看三个函数,它们分别带有0个、1个及2个参数:

目前我们只需要知道,传给函数的字符串在C语言里面用char*类型来表示。这种类型会在第3章讲解,而到了第15章,我们还会更加详细地解释与字符串有关的细节。大家在观察这些函数时,首先应该注意它有几个参数,然后详细观察每个参数的类型与标识符。
函数在它的函数体中不仅可以访问参数,还能操作这些参数。但是,它对参数值所做的修改只在函数体内部有效。一旦函数执行完毕,这些参数的值就会被丢弃。
下面这段程序演示了如何在函数体里面使用参数的值:

刚才这段代码的前两个函数,返回值类型都定义成了void,因此,对这两个函数来说,return;语句是可选的,于是,我们就把这条语句省略掉了。第一个函数没有参数,因此它的参数列表里面写的是void。第二个函数有一个参数,这个参数的标识符(也就是名称)叫作word,类型是char*。我们刚才说过,这个类型目前只需要理解成字符串就好。为了在调用printf()函数时正确使用word所表示的字符串,我们这次在调用printf()函数时需要传入两个参数,第一个参数里面有一个特殊的转义序列,也就是"%s",这种转义序列称为格式说明符或格式限定符(format specifier)。它的意思是让printf()函数把后面那个参数(即本例中的word)当作字符串(string),放置在%s所处的位置上。后面我们还会遇见其他一些格式说明符,等用到某个说明符的时候,笔者再跟大家解释它的意思。到第19章,我们会详细解释这些说明符。
与学习其他范例程序时类似,你需要把这段代码录入计算机,然后编译并运行程序,最后验证它的输出结果。这个程序输出的依然是Hello, world!。
有了上面的两个函数,我们就可以构建一个更加通用的欢迎函数了,这个函数能够把欢迎词说给特定的人听。我们想让这个函数(即printGreeting())接受两个参数值,一个用来表示欢迎词的开头部分(即greeting),另一个用来表示受欢迎的一方(即addressee)。下面我们新建一个名叫hello4.c的文件,并在里面编写这样的代码:

这次我们又在函数的参数列表里遇见了char*类型,跟刚才一样,大家还是暂且把它理解成字符串,详细的含义我们后面再讲。这个hello4.c程序把原来位于main()函数的主体部分中的那些逻辑代码移动到了新声明的这个printGreeting()函数里,并让这个函数接受两个字符串型的参数。把printGreeting()写好之后,就可以在main函数的函数体中多次调用它了,每次调用时,我们传入的都是内容各不相同的一对字符串。大家要注意,这对字符串需要分别括在双引号里面,而且它们之间要添加逗号,以表示这是两个不同的参数。另外还要注意,每条欢迎词的末尾都需要有(感叹号及)换行符,我们的程序只把打印(感叹号及)换行符的逻辑写了一次,然而却能让这四条欢迎词都具备这样的效果。现在请保存这份程序文件,然后编译并运行程序。你应该看到如下输出结果:

仔细观察这几个函数之间的运行方式,我们会发现,就算不写printComma()与printWord()这两个函数,依然可以编写这样一个通用的printGreeting()函数。为此,我们需要把那两个函数的功能合并成一条printf()语句,并在该语句中使用两个格式说明符来分别指代欢迎词的开头部分,以及接受欢迎词的那一方。现在就来编写这个新版的程序,我们复制hello4.c文件,并将这个副本命名为hello5.c,然后把其中的代码改成下面这样:

这个程序要比刚才那个简单,它只定义了一个带有双参数的函数,就实现了通用的致辞功能,而不像旧版那样,要定义三个函数。现在请保存这份文件,然后编译并运行程序。你看到的输出结果应该跟hello4.c程序的结果相同。
除了合并,我们还可以沿着相反的方向修改程序,也就是把打印欢迎词的功能拆分到许多个小的函数里面去实现。为此,我们将复制hello5.c文件,并将副本命名为hello6.c,然后把它的代码改成下面这样:


这个程序为了打印欢迎词,所使用的函数比hello4.c还多。这样做的好处是便于重复运用其中的那些小函数,而不用把相应的代码手工编写(或者手工复制)许多遍。比方说,我们可以扩充这个程序的功能,让它不仅能打印欢迎词,而且还能打印各种类型的句子,例如问句以及普通的句子等。这样我们或许可以实现一个能够处理(日常)语言并生成文本的程序。现在请编译hello6.c文件并运行该程序,你应该会看到下面这样的输出结果:

只为了灵活打印两个单词就定义这样一大批函数似乎有点多此一举。这样设计,实际上是想让你意识到同一个程序在函数上可以有好几种组织方式。你可以把程序的代码写在几个比较大的函数里面,让这些函数分别实现某个比较大的功能,也可以把程序的代码拆分成许多个比较小的函数,让每个小函数只实现一个具体的小功能,并通过调用这些小函数来实现某个大的功能。具体采用哪种划分方式应该根据你所要解决的问题来确定。总之,同一个程序可以用不同的结构来安排,很少会出现那种只能采用一种结构的情况。
你可能会问,为什么我们自己定义的这些函数,其参数个数都是固定的,而printf()函数却可以时而接受一个参数,时而接受两个参数呢?这是因为printf()是参数个数可变的函数(variadic function)。C语言里面专门有一种机制来处理这样的函数。我们不打算讲解这个机制,只会在附录G简单提一下涉及该机制的stdarg.h头文件。
为了区分函数与程序中的其他要素,笔者会采用name()这样的形式表示函数,也就是把一对括号写在函数标识符的右侧(用来强调name是一个函数的名称,而不是其他某种编程要素的名称)。
下面我们再总结一下函数与函数之间的关系:
□函数是用来被调用(called)的,调用该函数的那个函数称作该函数的调用方或主调方(caller)。例如printComma()函数被printGreeting()函数调用,因此,printGreeting()是printComma()的调用方。
□被调用的这个函数叫作受调用方或被调用方(callee),它在执行完必要的操作之后,需要把控制权返回给调用方。例如printComma()会把控制权返回给print-Greeting()函数,而printGreeting()函数又会把控制权返回给main()函数。
□主动调用(call)另一个函数的函数叫作调用方或主调方,而被调用的那个函数则是受调用方。例如main()函数调用printGreeting(),main()是主调方,printGreeting()是受调用方。printGreeting()调用printAddressee()函数,printGreeting()是主调方,printAddressee()函数是被调用方。
[1] 严格来说,还是(void)更加明确,写成()会给人一种“可以传参数也可以不传”的感觉。——译者注