把一段逻辑写进函数,就能重复调用、按需组合,这是 C 程序组织代码的基本方式。而递归——函数调用自己——则是解决"同构子问题"的利器。本文从函数的三要素讲起,一路讲到递归的经典案例。
1. 函数的三要素
一个函数由三部分构成:返回类型、函数名、参数列表。设计函数时问自己三个问题:它返回什么?它需要什么输入?它做什么?
#include <stdio.h>
// 返回两个整数中较大的一个
int max(int a, int b) {
if (a > b) {
return a;
}
return b;
}
int main(void) {
printf("max(3, 7) = %d\n", max(3, 7)); // 7
return 0;
}
2. 声明、定义与调用
如果函数定义写在调用之后,需要先"声明"告诉编译器它的签名;定义写在前面则不需要:
#include <stdio.h>
int add(int a, int b); // 函数声明(原型)
int main(void) {
int sum = add(3, 4);
printf("3 + 4 = %d\n", sum);
return 0;
}
int add(int a, int b) { // 函数定义
return a + b;
}
实际工程中,声明通常放在 .h 头文件里,定义放在 .c 文件里,方便多文件协作。
3. 参数传递:值传递
C 默认是值传递:参数是实参的副本,函数内修改参数不影响外面的变量:
#include <stdio.h>
void try_change(int x) {
x = 999;
}
int main(void) {
int a = 10;
try_change(a);
printf("a 仍然是 %d\n", a); // 10,没变
return 0;
}
想真正修改外面的变量,需要传"地址"(指针),后面的文章会详细讲。
4. 返回值与 void
需要给调用者结果时用 return 返回值;不需要返回任何东西的函数,返回类型写 void。返回值可以参与表达式运算:
#include <stdio.h>
double circle_area(double r) {
return 3.14159 * r * r;
}
int main(void) {
double total = circle_area(1.0) + circle_area(2.0);
printf("两个圆面积之和 = %.2f\n", total);
return 0;
}
5. 递归:函数调用自己
递归包含两部分:基准情形(不再递归的条件)和递推公式。以阶乘为例,n! = n × (n-1)!:
#include <stdio.h>
long long factorial(int n) {
if (n <= 1) {
return 1; // 基准情形
}
return n * factorial(n - 1); // 递推公式
}
int main(void) {
printf("10! = %lld\n", factorial(10)); // 3628800
return 0;
}
每次递归调用都会在栈上开辟新空间,递归太深可能导致栈溢出。写递归前先问:基准情形存在吗?每次调用都在向基准靠近吗?
6. 递归 vs 循环:斐波那契数列
斐波那契数列 F(n) = F(n-1) + F(n-2) 天然适合递归,但朴素递归存在大量重复计算;循环版本效率更高:
#include <stdio.h>
// 递归版:直观但慢
long long fib_rec(int n) {
if (n <= 1) return n;
return fib_rec(n - 1) + fib_rec(n - 2);
}
// 循环版:快
long long fib_loop(int n) {
long long a = 0, b = 1;
for (int i = 2; i <= n; i++) {
long long t = a + b;
a = b;
b = t;
}
return n == 0 ? a : b;
}
int main(void) {
printf("fib(30) 递归 = %lld\n", fib_rec(30));
printf("fib(30) 循环 = %lld\n", fib_loop(30));
return 0;
}
经验法则:问题结构本身就是"一棵树"(如目录遍历、快速排序)时用递归;只是简单重复计数时用循环。
💡 学习建议:用纸笔模拟一遍 factorial(4) 的调用过程,画出"调用栈"的进出顺序。能把递归过程画清楚,才算真正理解了递归。