为什么需要函数
到目前为止,所有代码都写在 main 里。程序只做一件事时这样没问题,可一旦要做的事多了,三个问题就会同时出现:同一段代码被复制到三个地方,main 长到没人能记得住,而且没法给一段代码起个名字,说明它是干什么用的。
函数能同时解决这三个问题。它给一段代码起一个名字,提供自己的局部变量、一组输入,以及一个要交回的值。
square 只写了一次,却调用了三次,其中一次是作用在它自己的结果上。其实你早就在调用函数了:printf 就是一个函数,main 也是。
函数的结构
returnType name(parameterList) {
// body
return value;
}
四个部分:
- 返回类型:函数交回的值的类型。可以是
int、double、char、指针类型,或者表示“什么都不返回”的void。 - 函数名:命名规则和变量名相同,应该说明函数做什么或产生什么。
- 参数列表:函数的输入,每个参数都有自己的类型,用逗号隔开。
(void)表示不接收参数。 - 函数体:写在大括号中的语句,除非返回类型是
void,否则以return结束。
注意,调用 pi() 时仍然需要空括号,正是括号让它成为一次调用,而不只是提到这个名字。而 printBanner(20); 自成一条语句,因为它不产生可供使用的值。
没有参数的函数,要写 (void) 而不是 ()。在 C 语言中两者含义不同,() 是比较宽松的那种,稍后会详细说明。
调用和返回
调用会把控制权转移到函数中,执行函数体,然后带着一个值返回,这个值会替换外层表达式中的这次调用。
return 同时做两件事:提供返回值,并且立即结束函数。同一条执行路径上 return 之后的任何代码都不会执行,larger 中提前返回的写法正是依靠这一点:不需要 else,因为能执行到最后一行,就说明 a > b 为假。
一个函数可以有多条 return 语句,但每次调用只会执行其中一条。
void 函数
为了效果而不是为了返回值而存在的函数,返回类型是 void:比如打印、通过指针更新某个值、绘图。
在 void 函数中,不带值的 return; 用于提前退出。在这里写 return someValue; 是编译错误,把调用用在表达式中也一样:int x = printTable(7, 5); 无法通过编译,这正是类型系统在发挥作用。
拆分程序:真正的意义
函数的主要意义并不是避免重复,而是把一个冗长的过程变成几个有名字的步骤,让 main 读起来像是对程序的描述,而不是它的实现细节。
现在 main 只有四行输出和一次计算。每个辅助函数只做一件事,名字说明了这件事是什么,而且可以单独测试。注意数组的长度是作为第二个参数传入的:数组一旦传给函数,sizeof 技巧在那里就不再有效,因为数组会退化为指针。
把工作拆分成函数时,有三条经验法则:
- 一个函数只做一件事。 如果名字里需要一个“和”字,它可能应该是两个函数。
- 需要什么就接收什么,产生什么就返回什么。 读取全局状态的函数更难推理,也无法单独测试。
- 短到一眼就能看完。 没有神奇的行数限制,但一个一屏放不下的函数,里面通常藏着一个更小的函数。
函数调用函数
函数可以调用任何已经声明过的函数,包括你自己写的:
main 调用 report,report 调用 sumEvens,sumEvens 又在循环中调用 isEven。每次调用都会得到一套全新的局部变量,叠放在调用者的变量之上,函数返回时就被丢弃。
上面的定义是按依赖顺序排列的:isEven 在 sumEvens 之前,sumEvens 在 report 之前,因为 C 语言要求函数先声明后调用。如果调用一个在文件更下方才出现的函数,编译器就会报告隐式声明。通用的解决办法是在文件顶部附近写函数原型,这样定义就可以按任意顺序排列。函数调用自己也是合法且有用的,那就是递归。
main 也是函数
main 是一个普通函数,由运行时替你调用。它的 int 返回类型就是程序的退出状态:0 表示成功,非零表示失败,shell 脚本和构建工具都会读取它。
int main(void) {
/* ... */
return 0;
}
从 C99 开始,执行到 main 末尾而没有 return 时,会隐式返回 0,这条特殊规则只适用于 main。其他所有非 void 函数如果没有返回值就结束了,而调用者又使用了结果,就会产生未定义行为。
另一种标准形式是 int main(int argc, char *argv[]),它接收命令行参数。两种都是正确的;void main() 不正确,不管老教程怎么说。
常见错误
- 非 void 函数忘了写
return。 调用者就会读到一个垃圾值。gcc -Wall会发出警告。 - 在函数体内声明了和参数同名的变量。 它会遮蔽参数,你本想对输入做的赋值就落空了。
- 以为函数能改变它的实参。 C 语言的一切都是值传递,所以函数收到的是副本。
void reset(int x) { x = 0; }不会改变调用处的任何东西,这正是下一页的主题。 - 调用时省略括号。
printBanner;是一个合法的表达式,它求出函数的地址然后丢弃。它能通过编译,什么也不做,-Wall会把它标记出来。 - 写
()而不是(void)。 这是合法的,但在较旧的 C 方言中会关闭参数检查。
常见问题
C 语言中怎么声明函数?
依次写出返回类型、函数名和括号里的参数列表,然后是大括号中的函数体:int add(int a, int b) { return a + b; }。不接收参数的函数用 (void),不返回任何值的函数返回类型为 void。
C 语言函数中 return 有什么作用?
它立即结束函数,并把一个值交还给调用者。这个值的类型必须和函数声明的返回类型一致。在 void 函数中,可以写一个单独的 return; 提前退出,也可以不写,让函数在右大括号处结束。
为什么 C 语言的 main 要返回 int?
main 的返回值就是程序的退出状态,操作系统和 shell 脚本都可以读取它。return 0; 表示成功,任何非零值表示失败。从 C99 开始,执行到 main 末尾而没有 return 语句时,会隐式返回 0。