3. 形参和实参

下面我们定义一个带参数的函数,我们需要在函数定义中指明参数的个数和每个参数的类型,定义参数就像定义变量一样,需要为每个参数指明类型,并起一个符合标识符命名规则的名字。例如:

例 3.4. 带参数的自定义函数

#include <stdio.h>

void print_time(int hour, int minute)
{
	printf("%d:%d\n", hour, minute);
}

int main(void)
{
	print_time(23, 59);
	return 0;
}

需要注意的是,定义变量时可以把同样类型的变量列在一起,而定义参数却不可以,例如下面这样的定义是错误的:

void print_time(int hour, minute)
{
	printf("%d:%d\n", hour, minute);
}

学习C语言的人肯定都乐意看到这句话:“变量是这样定义的,参数也是这样定义的,一模一样”,这意味着不用专门去记住参数应该怎么定义了。谁也不愿意看到这句话:“变量可以这样,而参数却不可以”。C语言的设计者也不希望自己设计的语法规则里到处都是这种例外,一个容易被用户接受的设计应该遵循最少例外原则(Rule of Least Surprise)。其实这里的这个规定也不算十分的例外,并不是C语言的设计者故意找茬,而是不得不这么规定,读者想想为什么呢?学习编程语言不应该死记各种语法规定,如果能够想清楚设计者这么规定的原因(Rationale),不仅有助于记忆,而且会有更多收获。本书在必要的地方会解释一些Rationale,或者启发读者自己去思考,例如先前在脚注中解释了void关键字的Rationale。

总的来说,C语言的设计是非常优美的,只要理解了少数的基本概念、基本原则就可以根据组合规则写出任意复杂的程序,极少有例外的规定说这样组合是不允许的,或者那样类推是错误的。相反,C++的设计就非常复杂,处处充满了例外,全世界没有几个人能把C++所有的规则都牢记于心的,因而不被人广泛接受。这个观点在[UNIX编程艺术]一书中有详细阐述。

在本书中,凡是提醒读者注意的地方都是多少有些Surprise的地方,初学者如果按常理来想很可能要出错,所以需要特别提醒一下。而初学者容易犯的另外一些错误,完全是因为没有掌握好基本概念和基本原理,或者根本无视组合规则而全凭自己主观臆断所致,对于这一类问题本书不会做特别的提醒,例如有的初学者看完第 2 章 常量、变量和表达式之后会这样打印π的值:

double pi=3.1416;
printf("pi\n");

之所以会犯这种错误,一是不理解Literal的含义,二是自己想当然地把变量名组合到字符串里去,而事实上根本没有这条组合规则。如果连这样的错误都需要在书上专门提醒,就好比提醒小孩吃饭一定要吃到嘴里去,不要吃到鼻子里去,更不要吃到耳朵里去一样。

回到正题。我们调用print_time(23, 59)时,函数中的参数hour就代表23,参数minute就代表59。确切地说,当我们讨论函数中的hour这个参数时,我们所说的“参数”是指形参(Parameter),当我们讨论传一个参数23给函数时,我们所说的“参数”是指实参(Argument),但是我习惯都叫参数而不习惯总把形参、实参这两个文绉绉的词挂在嘴边儿(事实上大多数人都不习惯),读者可根据上下文判断我指的到底是形参还是实参。记住这条基本原理:形参相当于函数中定义的变量,调用函数传递参数的过程相当于定义形参变量并且用实参的值来初始化。例如这样调用:

void print_time(int hour, int minute)
{
	printf("%d:%d\n", hour, minute);
}

int main(void)
{
	int h = 23, m = 59;
	print_time(h, m);
	return 0;
}

相当于在函数print_time中执行了这样一些语句:

int hour = h;
int minute = m;
printf("%d:%d\n", hour, minute);

main函数的变量h和print_time函数的参数hour是两个不同的变量,只不过它们各自的存储空间中存了相同的值23,因为变量h的值赋给了参数hour。同理,变量m的值赋给了参数minute。C语言的这种传递参数的方式称为Call by Value。在调用函数时,每个参数都需要得到一个值,函数定义中有几个Parameter,在调用中就需要传几个Argument,不能多也不能少,每个参数的类型也必须对应上。但是为什么我们调用printf函数时传的Argument数目是变化的,有时一个有时两个甚至更多个?这是因为C语言规定了一种特殊的参数列表格式,例如printf的原型是这样的:

int printf(const char *format, ...);

第一个参数是const char *类型的,后面的...可以代表0个或任意多个参数,这些参数的类型也是不确定的,这称为可变参数(Variable Argument),以后我们再详细讨论这种格式。总之,任何函数的定义既规定了返回值的类型,也规定了参数的类型和个数,即使像printf这样规定为“不确定”也是一种明确的规定,调用函数就要严格遵守这些规定,通常我们说函数提供了一个接口(Interface),调用函数就是使用这个接口,使用的前提是必须和接口保持一致。

习题

1、定义一个函数increment,它的作用是将传进来的参数加1,然后在main函数中用increment函数来增加变量的值:

void increment(int x)
{
	x = x + 1;
}

int main(void)
{
	int i = 1, j = 2;
	increment(i); /* now i becomes 2 */
	increment(j); /* now j becomes 3 */
	return 0;
}

这个increment函数能奏效吗?为什么?