14、函数(三):静态变量(static)、递归函数(阶乘/斐波那契)、递归与迭代的对比
好,咱们继续聊函数。前面两章我们把函数的参数、返回值、作用域都捋了一遍。这一章,我打算聊聊两个在实战中特别容易让人「栽跟头」的东西——静态变量和递归。
嗯,先别急着翻白眼。我知道递归这个概念,教科书上讲得玄乎其玄,什么「自己调用自己」之类的。但说白了,它就是个循环的另一种写法。只不过,这个循环是用函数调用栈来实现的。
咱们一个一个来。
静态变量(static)
先说说 static 这个关键字。很多人以为它只是用来修饰全局变量的,其实不然。它修饰局部变量时,效果完全不一样。
我举个例子。你写一个计数器函数,每次调用都加1。如果用普通局部变量,每次函数结束,变量就销毁了,下次调用又得重新初始化。这显然不行。
这时候,static 就派上用场了。
#include <stdio.h>
void counter() {
static int count = 0; // 静态局部变量
count++;
printf("第 %d 次调用\n", count);
}
int main() {
counter(); // 第 1 次调用
counter(); // 第 2 次调用
counter(); // 第 3 次调用
return 0;
}
你看,count 只初始化一次。之后每次调用,它都保留着上一次的值。这就是静态局部变量的核心特点:生命周期贯穿整个程序,但作用域仅限于函数内部。
核心要点:
- 静态局部变量在程序启动时分配内存,程序结束时才释放。
- 它只初始化一次,后续调用不再重新初始化。
- 它的作用域仍然是函数内部,外部无法直接访问。
我在项目中遇到过一种情况:一个嵌入式设备需要记录某个操作的总次数,但又不想用全局变量(怕被其他地方意外修改)。用 static 局部变量就完美解决了。既安全,又方便。
小技巧:如果你需要一个「函数级别的全局变量」,但又不想污染全局命名空间,就用 static 局部变量。这是 C 语言里很优雅的一种设计。
递归函数:阶乘与斐波那契
好,接下来是重头戏——递归。
递归,说白了就是一个函数调用它自己。但这里有个关键:必须有一个终止条件,否则就会无限递归,最终栈溢出。
我刚开始学递归的时候,总觉得这玩意儿很神奇。后来写多了才发现,它其实就是把一个大问题拆成一个个小问题,直到小到可以直接解决。
阶乘(Factorial)
阶乘是最经典的递归例子。n! = n * (n-1)!,边界条件是 0! = 1。
#include <stdio.h>
int factorial(int n) {
if (n == 0) {
return 1; // 终止条件
}
return n * factorial(n - 1); // 递归调用
}
int main() {
int n = 5;
printf("%d! = %d\n", n, factorial(n)); // 输出 120
return 0;
}
你看,代码很简洁。但简洁的背后,是函数调用栈在默默工作。每次递归调用,都会在栈上压入一个新的函数帧,保存当前的状态。等递归返回时,再一层层弹出。
注意:递归虽然代码简洁,但并不是万能的。如果递归深度太大,比如几万层,栈空间可能不够用,程序就会崩溃。我曾经在调试一个递归算法时,就因为没注意深度,导致栈溢出,查了半天才找到原因。
斐波那契数列(Fibonacci)
斐波那契数列是另一个经典例子。F(n) = F(n-1) + F(n-2),边界条件是 F(0)=0,F(1)=1。
#include <stdio.h>
int fibonacci(int n) {
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
int main() {
int n = 10;
printf("F(%d) = %d\n", n, fibonacci(n)); // 输出 55
return 0;
}
嗯,这里要注意。这个递归实现虽然直观,但效率极低。为什么?因为它会重复计算很多次。比如计算 F(5),它会计算 F(4) 和 F(3),而 F(4) 又会计算 F(3) 和 F(2)……你想想看,F(3) 被重复计算了多少次?
这就是递归的一个典型问题:重复计算。对于斐波那契数列,递归的时间复杂度是 O(2^n),指数级的增长,n 稍微大一点就扛不住了。
递归与迭代的对比
好,既然递归有这些问题,那有没有更好的办法?当然有——迭代。
迭代,说白了就是用循环来解决问题。同样是斐波那契数列,用迭代写出来是这样的:
#include <stdio.h>
int fibonacci_iter(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
int a = 0, b = 1, c;
for (int i = 2; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}
int main() {
int n = 10;
printf("F(%d) = %d\n", n, fibonacci_iter(n)); // 输出 55
return 0;
}
你看,迭代版本的时间复杂度是 O(n),空间复杂度是 O(1)。相比之下,递归版本的时间复杂度是 O(2^n),空间复杂度是 O(n)(因为调用栈深度)。
我个人的习惯是:能用迭代解决的问题,尽量不用递归。除非递归的代码特别简洁,而且递归深度可控,我才会考虑用递归。
下面这张图,可以帮你更直观地理解递归和迭代的区别:
你看,递归和迭代各有各的适用场景。递归适合那些天然具有递归结构的问题,比如树的遍历、图的深度优先搜索。而迭代则更适合线性问题,比如数组求和、链表遍历。
总结一下:
- 静态变量:生命周期贯穿程序,作用域限于函数内部。适合做函数级别的计数器或状态保存。
- 递归:代码简洁,但要注意深度和重复计算问题。适合树、图等递归结构。
- 迭代:效率高,空间省。适合线性问题和大规模计算。
我的建议:面试时如果遇到递归问题,先别急着写递归。先想想能不能用迭代实现。如果能,优先写迭代。如果面试官要求递归,再写递归。这样既能展示你的思维广度,又能体现你的工程经验。
好,这一章就到这里。静态变量和递归,说白了都是 C 语言里很基础但又很实用的东西。你想想看,掌握了这些,面试官再问你「递归和迭代有什么区别」,你就能从原理、效率、适用场景等多个角度去回答了。
嗯,下一章我们聊聊指针。那才是 C 语言的精髓所在。