第9章 函数(二):局部变量与全局变量、变量的作用域与生命周期、静态变量static、递归函数

各位同学,咱们继续聊函数。上一章我们把函数的定义、声明、参数传递这些基本功讲完了。这一章要深入一点,聊聊变量在函数里到底是怎么“活”的,以及一个很酷的技巧——递归。

说实话,我刚学C语言那会儿,最头疼的就是“作用域”和“生命周期”这两个概念。明明都是变量,为什么有的函数里能用,换个函数就不认识了?为什么有的变量函数一结束就没了,有的却一直赖着不走?

别急,今天咱们就把这些事彻底捋清楚。

9.1 局部变量与全局变量

先看个最简单的例子:

#include <stdio.h>

int global_var = 100;  // 全局变量

void func() {
    int local_var = 200;  // 局部变量
    printf("局部变量: %d\n", local_var);
    printf("全局变量: %d\n", global_var);
}

int main() {
    func();
    // printf("%d\n", local_var);  // 这行会报错!local_var 在这里不可见
    printf("全局变量: %d\n", global_var);
    return 0;
}

局部变量:定义在函数内部的变量。它的作用域仅限于这个函数。函数一结束,它就“消失”了。我在项目中见过不少新手,在main函数里想访问另一个函数里的局部变量,结果编译报错,一脸懵。

全局变量:定义在所有函数之外的变量。从定义位置开始,到文件结束,所有函数都能访问它。听起来很方便对吧?但我要提醒你:全局变量是把双刃剑

我曾经接手过一个项目,前任工程师用了二十多个全局变量。调试的时候,一个变量被改了,根本不知道是哪个函数干的。查了三天才找到bug。从那以后,我对全局变量就特别谨慎。

我的建议是:能用局部变量解决的问题,就别用全局变量。全局变量只适合那些真正需要“全局共享”的数据,比如系统配置参数、硬件状态标志等。

9.2 变量的作用域与生命周期

这两个概念经常被混为一谈,其实它们是两码事。

  • 作用域:变量在代码的哪些地方可以被访问。说白了就是“看得见摸得着”的范围。
  • 生命周期:变量从创建到销毁的这段时间。说白了就是“活着”的时间段。

咱们用一张图来理解:

变量的作用域与生命周期 全局变量 作用域:从定义位置到文件结束 生命周期:程序启动到程序结束 局部变量 作用域:仅限于定义它的函数内部 生命周期:函数调用时创建,函数返回时销毁 静态局部变量 (static) 作用域:仅限于定义它的函数内部 生命周期:程序启动到程序结束(只初始化一次)

你看,全局变量活最久,但范围也最广。局部变量范围小,活得也短。静态局部变量呢?范围小,但活得久——这个我们马上讲。

9.3 静态变量 static

static 这个关键字,在C语言里是个“多面手”。用在局部变量上,效果很特别。

#include <stdio.h>

void counter() {
    static int count = 0;  // 静态局部变量
    count++;
    printf("第 %d 次调用\n", count);
}

int main() {
    counter();  // 输出:第 1 次调用
    counter();  // 输出:第 2 次调用
    counter();  // 输出:第 3 次调用
    return 0;
}

看到了吗?普通局部变量每次函数调用都会重新创建,但 static 变量只初始化一次。函数返回后,它的值依然保留。下次再调用这个函数,count 还是上次的值。

我个人习惯用 static 变量来记录函数被调用的次数,或者保存一些需要“记住”的状态。比如在嵌入式开发中,我经常用 static 变量来保存按键的消抖状态。

不过要注意:static 变量虽然生命周期长,但作用域没变。它还是只能在定义它的函数里访问。你不能在 main 函数里直接访问 counter 函数里的 count 变量——编译器会毫不留情地报错。

9.4 递归函数

递归,说白了就是“函数调用自己”。听起来有点绕,但用对了地方,代码会非常简洁优雅。

先看一个经典例子——计算阶乘:

#include <stdio.h>

int factorial(int n) {
    if (n <= 1) {
        return 1;  // 递归终止条件
    }
    return n * factorial(n - 1);  // 递归调用
}

int main() {
    int result = factorial(5);
    printf("5! = %d\n", result);  // 输出:5! = 120
    return 0;
}

递归函数有两个关键点:

  • 递归终止条件:必须有一个条件让递归停下来,否则会无限递归,最终栈溢出。
  • 递归调用:每次调用都向终止条件靠近一步。

咱们用图来理解 factorial(4) 的执行过程:

递归调用过程:factorial(4) factorial(4) factorial(3) factorial(2) factorial(1) 返回 1 返回 2×1=2 返回 3×2=6 返回 4×6=24 先一层层向下调用,直到终止条件;再一层层向上返回结果

你看,递归的过程就像“先往下挖,挖到底,再一层层往上返”。

我在项目中遇到过一个场景:需要遍历一个文件系统的目录树。用递归写,代码不到20行。如果用循环加栈模拟,至少得写60行。递归的优雅之处就在这里——它能把复杂问题简单化。

但递归也有代价:每次函数调用都会消耗栈空间。如果递归层数太深(比如几万层),栈会溢出。嵌入式开发中,栈空间本来就有限,用递归要格外小心。

9.5 避坑指南

最后,分享几个我踩过的坑:

  1. 全局变量滥用:我曾经在一个通信协议栈里用了全局变量来保存状态,结果多线程环境下数据全乱了。后来改成函数参数传递,问题解决。
  2. 递归忘记终止条件:有一次写递归,忘了写 if 判断,程序直接崩溃。调试了半天才发现是栈溢出了。
  3. static 变量误解:有人以为 static 变量作用域也是全局的,结果在另一个文件里 extern 引用,编译报错。记住:static 只是生命周期变长,作用域不变。
  4. 局部变量返回地址:千万不要返回局部变量的地址!函数结束后,局部变量的内存就被回收了,返回的指针成了野指针。

好了,这一章的内容就到这里。变量怎么活、怎么死,递归怎么用,心里有数了吧?代码写多了,这些概念自然就熟了。多练练,别怕犯错——我当年也是这么过来的。


公众号:蓝海资料掘金营,微信deep3321