把一段逻辑写进函数,就能重复调用、按需组合,这是 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) 的调用过程,画出"调用栈"的进出顺序。能把递归过程画清楚,才算真正理解了递归。