C: 函数进阶

递归就像两面相对的镜子——图像不断嵌套自身,直到远处的光点消失。函数调用自身解决问题,靠的就是这种层层缩小的力量。

1. 递归原理

递归是函数直接或间接调用自身的编程技术。每个递归必须具备两个要素:

  1. 递归条件:问题可以分解为规模更小的同类子问题
  2. 终止条件(基线条件):最小子问题有直接答案,不再递归
C
int factorial(int n) {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n - 1);
}

factorial(4)为例,执行过程:

C
factorial(4)
= 4 * factorial(3)
= 4 * 3 * factorial(2)
= 4 * 3 * 2 * factorial(1)
= 4 * 3 * 2 * 1
= 24
⚠️ 缺少终止条件的递归会无限调用,直到栈溢出崩溃。写递归时首先确定终止条件。


2. 经典递归案例

(1) 阶乘

n的阶乘定义:n! = n × (n-1)!,且0! = 1

C
long factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

注意long类型能容纳的范围有限,factorial(20)已接近64位上限。

(2) 斐波那契数列

斐波那契定义:F(1)=1, F(2)=1, F(n)=F(n-1)+F(n-2)

C
long fib(int n) {
    if (n <= 2) return 1;
    return fib(n - 1) + fib(n - 2);
}
🔥 朴素递归求斐波那契效率极低!fib(40)需要上亿次调用,因为大量子问题被重复计算。实际应用中应该用循环或记忆化优化。

(3) 汉诺塔

将n个盘子从柱A移到柱C,借助柱B,规则:大盘不能放在小盘上。

C
#include <stdio.h>

void hanoi(int n, char from, char mid, char to) {
    if (n == 1) {
        printf("%c -> %c\n", from, to);
        return;
    }
    hanoi(n - 1, from, to, mid);
    printf("%c -> %c\n", from, to);
    hanoi(n - 1, mid, from, to);
}

int main(void) {
    hanoi(3, 'A', 'B', 'C');
    return 0;
}
TEXT 📖 仅展示
A -> C
A -> B
C -> B
A -> C
B -> A
B -> C
A -> C

思路:先把上面n-1个盘子移到辅助柱,把最大盘移到目标柱,再把n-1个盘子从辅助柱移到目标柱。3个盘子需要7步,n个盘子需要2^n - 1步。

▶ 示例

递归求整数各位数字之和:

C
#include <stdio.h>

int digit_sum(int n) {
    if (n < 10) return n;
    return n % 10 + digit_sum(n / 10);
}

int main(void) {
    printf("%d\n", digit_sum(12345));
    printf("%d\n", digit_sum(7));
    return 0;
}
▶ 试一试
TEXT 📖 仅展示
15
7

digit_sum(12345) = 5 + digit_sum(1234) = 5 + 4 + digit_sum(123) = ... = 5+4+3+2+1 = 15


3. 变量作用域

作用域决定变量在哪些代码区域可见可用。

(1) 块作用域

{}内定义的变量只在该块内可见,块结束即销毁:

C
int main(void) {
    int x = 10;
    {
        int y = 20;
        printf("%d %d\n", x, y);
    }
    printf("%d\n", x);
    return 0;
}

y只在内层块中存在,外层无法访问。

(2) 文件作用域

在所有函数外定义的变量具有文件作用域,从定义位置到文件末尾都可见,称为全局变量

C
int count = 0;

void increment(void) {
    count++;
}

int main(void) {
    increment();
    increment();
    printf("%d\n", count);
    return 0;
}
⚠️ 全局变量虽然方便,但容易被任何函数意外修改,增加调试难度。能用局部变量解决的,不要用全局变量。

(3) 同名遮蔽

内层作用域的变量会遮蔽外层同名变量:

C
int x = 100;

int main(void) {
    int x = 10;
    printf("%d\n", x);
    return 0;
}

输出10,内层x遮蔽了全局x。全局x在内层块中无法直接访问。


4. 存储类

存储类决定变量的生命周期和可见性。C语言有四个存储类关键字:autostaticexternregister

(1) auto

auto是局部变量的默认存储类,通常省略不写。变量在进入块时创建,退出块时销毁:

C
void func(void) {
    auto int x = 10;
}

等价于int x = 10;

(2) static

static修饰局部变量时,变量在程序运行期间始终存在,但作用域仍限于函数内部。关键特性:只初始化一次,后续调用保留上次的值

C
#include <stdio.h>

void counter(void) {
    static int count = 0;
    count++;
    printf("第 %d 次调用\n", count);
}

int main(void) {
    counter();
    counter();
    counter();
    return 0;
}
TEXT 📖 仅展示
第 1 次调用
第 2 次调用
第 3 次调用

如果没有staticcount每次都会重新初始化为0,永远只打印"第1次"。

static修饰全局变量或函数时,将其可见性限制在当前文件内(内部链接),其他源文件无法通过extern访问。

(3) extern

extern用于声明一个在其他源文件中定义的全局变量,告诉编译器"这个变量存在,但不在当前文件定义":

file1.c:

C
int shared_data = 42;

file2.c:

C
#include <stdio.h>

extern int shared_data;

int main(void) {
    printf("%d\n", shared_data);
    return 0;
}

extern不分配内存,只是声明。多文件项目中共享全局数据时需要extern

(4) register

register建议编译器将变量存放在CPU寄存器中以提高访问速度:

C
void fast_loop(void) {
    register int i;
    for (i = 0; i < 1000000; i++) {
    }
}

现代编译器的优化能力很强,通常会自动把频繁使用的变量放入寄存器,register提示的作用已不大。注意:不能对register变量取地址(&),因为寄存器没有内存地址。

▶ 示例

static变量实现斐波那契(避免递归的重复计算):

C
#include <stdio.h>

long fib_iter(int n) {
    if (n <= 2) return 1;
    long prev = 1;
    long curr = 1;
    long next;
    int i;

    for (i = 3; i <= n; i++) {
        next = prev + curr;
        prev = curr;
        curr = next;
    }
    return curr;
}

int main(void) {
    int i;
    for (i = 1; i <= 10; i++) {
        printf("F(%d) = %ld\n", i, fib_iter(i));
    }
    return 0;
}
▶ 试一试
TEXT 📖 仅展示
F(1) = 1
F(2) = 1
F(3) = 2
F(4) = 3
F(5) = 5
F(6) = 8
F(7) = 13
F(8) = 21
F(9) = 34
F(10) = 55

这个迭代版本时间复杂度O(n),远优于朴素递归的O(2^n)。

❓ 常见问题

Q 递归和循环哪个更好?
A 循环效率通常更高,递归代码更直观。能用循环简单实现的优先用循环;树遍历等天然递归结构用递归更自然。
Q static局部变量存在内存的哪个区域?
A 静态存储区(数据段),和全局变量存放在同一区域,不在栈上。
Q extern声明和变量定义有什么区别?
A 定义分配内存并可能初始化,extern声明不分配内存只告知变量已在别处定义。同一变量只能定义一次,可以声明多次。
Q register变量真的更快吗?
A 现代编译器会自动做寄存器分配优化,显式register几乎无额外效果,这个关键字主要具有历史意义。

📖 小节

📝 作业

  1. 编写递归函数void print_reverse(int n),逆序输出正整数各位(如123输出3 2 1)。
  2. 编写函数用static变量统计自身被调用的次数,并在main中调用5次后打印统计结果。
  3. 用递归实现二分查找:在已排序数组中查找目标值,找到返回下标,未找到返回-1。
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏