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)。其实关于参数的这条规定也不算十分例外,也是可以理解的,请读者想想为什么要这么规定。学习编程语言不应该死记各种语法规定,如果能够想清楚设计者这么规定的原因(Rationale),不仅有助于记忆,而且会有更多收获。本书在必要的地方会解释一些 Rationale,或者启发读者自己去思考,例如上一节在脚注中解释了 void 关键字的 Rationale。C99 Rationale 是随 C99 标准一起发布的,值得参考。
总的来说,C 语言的设计是非常优美的,只要理解了少数基本概念和基本原则就可以根据组合规则写出任意复杂的程序,很少有例外的规定说这样组合是不允许的,或者那样类推是错误的。相反,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。在调用函数时,每个参数都需要得到一个值,函数定义中有几个形参,在调用时就要传几个实参,不能多也不能少,每个参数的类型也必须对应上。
肯定有读者注意到了,为什么我们每次调用 printf 传的实参个数都不一样呢?因为 C 语言规定了一种特殊的参数列表格式,用命令 man 3 printf 可以查看到 printf 函数的原型:
int printf(const char *format, ...);第一个参数是 const char * 类型的,后面的...可以代表 0 个或任意多个参数,这些参数的类型也是不确定的,这称为可变参数(Variable Argument),第 6 节“可变参数” 将会详细讨论这种格式。总之,每个函数的原型都明确规定了返回值类型以及参数的类型和个数,即使像 printf 这样规定为“不确定”也是一种明确的规定,调用函数时要严格遵守这些规定,有时候我们把函数叫做接口(Interface),调用函数就是使用这个接口,使用接口的前提是必须和接口保持一致。
Man Page
Man Page 是 Linux 开发最常用的参考手册,由很多页面组成,每个页面描述一个主题,这些页面被组织成若干个 Section。FHS(Filesystem Hierarchy Standard)标准规定了 Man Page 各 Section 的含义如下:
表 3.1. Man Page 的 Section
| Section | 描述 |
|---|---|
| 1 | 用户命令,例如 ls(1) |
| 2 | 系统调用,例如 _exit(2) |
| 3 | 库函数,例如 printf(3) |
| 4 | 特殊文件,例如 null(4) 描述了设备文件 /dev/null 、 /dev/zero 的作用 |
| 5 | 系统配置文件的格式,例如 passwd(5) 描述了系统配置文件 /etc/passwd 的格式 |
| 6 | 游戏 |
| 7 | 其它杂项,例如 bash-builtins(7) 描述了 bash 的各种内建命令 |
| 8 | 系统管理命令,例如 ifconfig(8) |
注意区分用户命令和系统管理命令,用户命令通常位于 /bin 和 /usr/bin 目录,系统管理命令通常位于 /sbin 和 /usr/sbin 目录,一般用户可以执行用户命令,而执行系统管理命令经常需要 root 权限。系统调用和库函数的区别将在 第 2 节“ main 函数和启动例程”说明。
Man Page 中有些页面有重名,比如敲 man printf 命令看到的并不是 C 函数 printf ,而是位于第 1 个 Section 的系统命令 printf ,要查看位于第 3 个 Section 的 printf 函数应该敲 man 3 printf ,也可以敲 man -k printf 命令搜索哪些页面的主题包含 printf 关键字。本书会经常出现类似 printf(3) 这样的写法,括号中的 3 表示 Man Page 的第 3 个 Section,或者表示“我这里想说的是 printf 库函数而不是 printf 命令”。
习题
定义一个函数
increment,它的作用是把传进来的参数加 1。例如:cvoid increment(int x) { x = x + 1; } int main(void) { int i = 1, j = 2; increment(i); /*i now becomes 2 */ increment(j); /* j now becomes 3*/ return 0; }我们在
main函数中调用increment增加变量i和j的值,这样能奏效吗?为什么?如果在一个程序中调用了
printf函数却不包含头文件,例如int main(void) { printf("\n"); },编译时会报警告:warning: incompatible implicit declaration of built-in function ‘printf’。请分析错误原因。