Menu

C 语言函数:声明、调用和返回值

如何在 C 语言中定义和调用函数:返回类型、参数、返回值、void 函数、为什么要拆分程序,以及一个由多个命名小函数组成的完整示例。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

为什么需要函数

到目前为止,所有代码都写在 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。

C 语言函数能返回多个值吗?

不能直接返回,return 只能产生一个值。常见的变通办法是返回一个包含多个字段的 结构体,或者把指针作为 参数 传入,让函数通过指针写入额外的结果。

Coddy 编程语言插图

和 Coddy 一起学编程

开始