10、函数(二):局部变量与全局变量、变量的作用域与生命周期、静态变量(static)、递归函数(阶乘、斐波那契数列)

好,咱们接着聊函数。上一章我们把函数的定义、声明、参数传递这些基本功讲透了。这一章,咱们要深入函数内部,看看那些变量到底是怎么活、怎么死的。说白了,就是变量的“地盘”和“寿命”问题。

我在项目里见过不少新手,甚至工作两三年的同事,在这上面栽跟头。一个全局变量被到处改,最后查 bug 查到怀疑人生。嗯,咱们今天就把这些坑填平。

函数(二)核心知识体系 变量作用域与生命周期 局部变量 作用域:函数内部 生命周期:函数调用期间 全局变量 作用域:整个文件 生命周期:程序运行期间 静态变量 static 作用域:局部/文件 生命周期:程序运行期间 递归函数 阶乘、斐波那契数列 核心:递归出口 + 递归公式

局部变量:函数里的临时工

局部变量,就是在函数内部定义的变量。它的作用域仅限于当前函数,出了这个函数,它就不认了。

你想想看,你在一个函数里定义了个 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_callscount 的值都在累加。这就是静态变量的特性——它不会像普通局部变量那样每次调用都重新创建。

小技巧:我习惯用静态局部变量来实现“函数级别的全局状态”。比如记录某个函数被调用的次数,或者缓存一些计算结果。这样既避免了全局变量的污染,又保留了状态。

静态全局变量呢?举个例子:

// file1.c
static int secret = 42;  // 只有 file1.c 能看到

void show_secret() {
    printf("%d\n", secret);
}

// file2.c
extern int secret;  // 这行会报错!因为 secret 是 static 的
// 编译时链接器会告诉你:找不到符号

我在做模块化开发时,经常用 static 全局变量来隐藏内部实现细节。对外只暴露接口函数,内部数据用 static 锁死。这样别人用你的代码时,想乱改都改不了。

递归函数:函数调用自己

递归,说白了就是函数自己调用自己。听起来有点绕,但用好了非常优雅。

递归有两个核心要素:

  1. 递归出口:什么时候停下来。没有出口就是死循环。
  2. 递归公式:如何把大问题拆成小问题。

阶乘:递归的经典入门

阶乘的数学定义: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