10、函数(二):局部变量与全局变量、变量的作用域与生命周期、静态变量(static)、递归函数(阶乘、斐波那契数列)
好,咱们接着聊函数。上一章我们把函数的定义、声明、参数传递这些基本功讲透了。这一章,咱们要深入函数内部,看看那些变量到底是怎么活、怎么死的。说白了,就是变量的“地盘”和“寿命”问题。
我在项目里见过不少新手,甚至工作两三年的同事,在这上面栽跟头。一个全局变量被到处改,最后查 bug 查到怀疑人生。嗯,咱们今天就把这些坑填平。
局部变量:函数里的临时工
局部变量,就是在函数内部定义的变量。它的作用域仅限于当前函数,出了这个函数,它就不认了。
你想想看,你在一个函数里定义了个 int a = 10;,在另一个函数里也定义了个 int a = 20;,这俩 a 完全没关系,各活各的。这就是局部变量的好处——隔离性。
核心要点:局部变量存储在栈区,函数调用时分配内存,函数返回时自动释放。
#include <stdio.h>
void func1() {
int x = 100; // 局部变量
printf("func1 中的 x = %d\n", x);
}
void func2() {
int x = 200; // 和 func1 的 x 是两码事
printf("func2 中的 x = %d\n", x);
}
int main() {
func1();
func2();
// printf("%d\n", x); // 这行会报错!x 在这里不可见
return 0;
}
我在项目中遇到过一个问题:一个同事在多个函数里用了同名变量,以为它们是同一个,结果数据对不上。排查了半天,其实就是局部变量的隔离性在起作用。记住:同名不同命,各管各的。
全局变量:程序里的公共资源
全局变量定义在函数外面,所有函数都能访问它。听起来很方便对吧?但方便的背后往往藏着陷阱。
警告:全局变量是一把双刃剑。用好了提高效率,用不好就是 bug 的温床。我建议:能不用就不用,非要用就加 static 限制作用域。
#include <stdio.h>
int counter = 0; // 全局变量
void increment() {
counter++; // 所有函数都能改它
}
void reset() {
counter = 0;
}
int main() {
increment();
increment();
printf("counter = %d\n", counter); // 输出 2
reset();
printf("counter = %d\n", counter); // 输出 0
return 0;
}
全局变量的生命周期是整个程序运行期间。程序启动时分配内存,程序结束时才释放。这意味着它一直占着内存,不会像局部变量那样自动回收。
我曾经在一个嵌入式项目里,因为全局变量被中断服务程序和主循环同时修改,导致数据错乱。那 bug 查了我整整两天。后来我加了个 volatile 关键字,才稳住局面。嗯,这里先卖个关子,volatile 咱们后面再细讲。
静态变量(static):低调的持久派
静态变量是个有意思的角色。它有两种用法:
- 静态局部变量:作用域在函数内部,但生命周期是整个程序运行期间。
- 静态全局变量:作用域限制在当前文件,其他文件无法访问。
说白了,静态变量就是“地盘小,活得久”。
#include <stdio.h>
void count_calls() {
static int count = 0; // 静态局部变量,只初始化一次
count++;
printf("函数被调用了 %d 次\n", count);
}
int main() {
count_calls(); // 输出 1
count_calls(); // 输出 2
count_calls(); // 输出 3
return 0;
}
你看,每次调用 count_calls,count 的值都在累加。这就是静态变量的特性——它不会像普通局部变量那样每次调用都重新创建。
小技巧:我习惯用静态局部变量来实现“函数级别的全局状态”。比如记录某个函数被调用的次数,或者缓存一些计算结果。这样既避免了全局变量的污染,又保留了状态。
静态全局变量呢?举个例子:
// file1.c
static int secret = 42; // 只有 file1.c 能看到
void show_secret() {
printf("%d\n", secret);
}
// file2.c
extern int secret; // 这行会报错!因为 secret 是 static 的
// 编译时链接器会告诉你:找不到符号
我在做模块化开发时,经常用 static 全局变量来隐藏内部实现细节。对外只暴露接口函数,内部数据用 static 锁死。这样别人用你的代码时,想乱改都改不了。
递归函数:函数调用自己
递归,说白了就是函数自己调用自己。听起来有点绕,但用好了非常优雅。
递归有两个核心要素:
- 递归出口:什么时候停下来。没有出口就是死循环。
- 递归公式:如何把大问题拆成小问题。
阶乘:递归的经典入门
阶乘的数学定义:n! = n * (n-1)!,且 0! = 1。
#include <stdio.h>
long factorial(int n) {
if (n == 0) { // 递归出口
return 1;
}
return n * factorial(n - 1); // 递归公式
}
int main() {
int n = 5;
printf("%d! = %ld\n", n, factorial(n)); // 输出 120
return 0;
}
执行过程是这样的:factorial(5) 调用 factorial(4),factorial(4) 调用 factorial(3)……一直调用到 factorial(0) 返回 1,然后一层层往回返。就像叠罗汉,一层层叠上去,再一层层拆下来。
斐波那契数列:递归的典型应用
斐波那契数列:1, 1, 2, 3, 5, 8, 13, 21...,每个数等于前两个数之和。
#include <stdio.h>
long fibonacci(int n) {
if (n == 1 || n == 2) { // 递归出口
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2); // 递归公式
}
int main() {
int n = 10;
printf("fibonacci(%d) = %ld\n", n, fibonacci(n)); // 输出 55
return 0;
}
注意:这个递归实现虽然直观,但效率很低。因为有很多重复计算。比如算 fibonacci(5) 时,fibonacci(3) 被算了两次。n 越大,重复计算越恐怖。我建议:实际项目中用循环或者动态规划代替纯递归。
我曾经在一个面试题里看到过这个坑:让候选人写斐波那契,递归版本写出来很容易,但问时间复杂度就懵了。递归的斐波那契是指数级复杂度 O(2^n),而循环版本是 O(n)。差距有多大?n=50 时,递归可能跑到地老天荒,循环瞬间出结果。
变量的生命周期对比
| 变量类型 | 存储位置 | 作用域 | 生命周期 | 默认初始值 |
|---|---|---|---|---|
| 普通局部变量 | 栈 | 函数内部 | 函数调用期间 | 随机值(垃圾值) |
| 静态局部变量 | 静态存储区 | 函数内部 | 程序运行期间 | 0 |
| 全局变量 | 静态存储区 | 整个文件 | 程序运行期间 | 0 |
| 静态全局变量 | 静态存储区 | 当前文件 | 程序运行期间 | 0 |
这张表我建议你记下来。面试时经常被问到,项目里也天天用。尤其是那个“默认初始值”——局部变量不初始化就是垃圾值,全局变量和静态变量不初始化就是 0。这个区别,我见过太多人搞混了。
我的习惯:所有变量都显式初始化。局部变量定义时就给个初值,哪怕就是 0。这样代码更安全,也更容易读。别偷懒,偷懒一时爽,debug 火葬场。
好了,这一章的内容就到这。局部变量、全局变量、静态变量、递归函数,这几个概念是 C 语言的基石。你想想看,变量怎么活、活多久、在哪儿活,搞清楚了,写代码心里就有底了。
递归函数虽然看起来酷,但别滥用。能用循环解决的问题,优先用循环。递归更适合树、图这种天然有递归结构的数据。咱们后面讲数据结构时会大量用到递归,到时候再深入。
公众号:蓝海资料掘金营,微信deep3321