11. 函数(二):局部变量与全局变量、变量的存储类别、递归函数、函数指针
好,我们接着聊函数。上一章我们把函数的定义、声明、参数传递这些基本功过了一遍。这一章要聊的,是真正让C语言函数变得“有灵魂”的东西——变量的作用域与生命周期、递归的思维、还有函数指针这种高阶玩法。
我个人习惯把这一章叫做“函数的进阶三件套”。你把这三点吃透了,写出来的代码会从“能跑”变成“有设计感”。
11.1 局部变量与全局变量
先说说变量在函数里的“地盘”问题。
局部变量,就是定义在函数内部的变量。它的作用域仅限于当前函数。说白了,你在函数A里定义的int a,在函数B里完全看不见。我刚开始学的时候,觉得这很自然——各管各的嘛。
全局变量,定义在所有函数之外。任何函数都能访问它。听起来很方便对吧?但我在项目中吃过亏——全局变量用多了,代码就像一团乱麻,一个地方改了值,另一个地方莫名其妙就崩了。
核心区别:局部变量活在“栈”上,函数结束就销毁。全局变量活在“静态存储区”,程序启动就存在,直到程序结束。
#include <stdio.h>
int global_count = 0; // 全局变量
void increment() {
int local_count = 0; // 局部变量
local_count++;
global_count++;
printf("local: %d, global: %d\n", local_count, global_count);
}
int main() {
increment(); // local: 1, global: 1
increment(); // local: 1, global: 2
increment(); // local: 1, global: 3
return 0;
}
看到没?每次调用increment,local_count都从0开始,而global_count一直累加。这就是生命周期的差异。
避坑指南:我曾经在一个多文件项目中,把全局变量定义在头文件里,结果链接时报了一堆“重复定义”错误。记住:全局变量只能在.c文件中定义,在.h文件中用extern声明。
11.2 变量的存储类别
C语言给了我们四种存储类别:auto、register、static、extern。说实话,auto基本没人用,因为默认就是auto。register在现代编译器面前也基本失效了——编译器比你更懂寄存器分配。
真正需要你上心的,是static和extern。
static:
- 修饰局部变量:让它变成“静态局部变量”。函数退出时,它不销毁,下次调用时值还在。但作用域还是仅限于函数内部。
- 修饰全局变量/函数:限制作用域为当前文件。其他文件无法访问。
extern:
- 告诉编译器:“这个变量/函数在其他文件里定义,别报错。”说白了就是跨文件引用。
// file1.c
#include <stdio.h>
static int hidden = 42; // 只有本文件能访问
void show_hidden() {
printf("hidden = %d\n", hidden);
}
// file2.c
extern void show_hidden(); // 声明来自其他文件的函数
int main() {
show_hidden(); // 可以调用
// printf("%d\n", hidden); // 错误!hidden是static,外部不可见
return 0;
}
我的习惯:在嵌入式项目中,我几乎把所有全局变量都加上static。只有通过接口函数暴露出去。这样能有效防止别人乱改你的内部状态。
11.3 递归函数
递归,说白了就是“函数调用自己”。听起来有点绕,但用对了地方,代码会非常优雅。
递归有两个关键点:
- 递归公式:把大问题拆成小问题,小问题和原问题是同类问题。
- 终止条件:必须有一个明确的出口,否则会无限递归,栈溢出。
最经典的例子:阶乘。
#include <stdio.h>
int factorial(int n) {
if (n <= 1) {
return 1; // 终止条件
}
return n * factorial(n - 1); // 递归公式
}
int main() {
printf("5! = %d\n", factorial(5)); // 120
return 0;
}
为什么会这样?你想想看:factorial(5) 调用 factorial(4),factorial(4) 调用 factorial(3)……一直降到 factorial(1) 返回1,然后逐层返回计算结果。这就是递归的“递推”和“回归”。
注意:递归不是万能的。每次递归调用都会消耗栈空间。深度太大(比如几万层),栈会爆掉。我在项目中用递归处理目录树时,就遇到过栈溢出——后来改成了迭代+栈数据结构。
再来看一个更实用的例子:斐波那契数列。
int fib(int n) {
if (n <= 1) {
return n;
}
return fib(n - 1) + fib(n - 2);
}
这个写法虽然简单,但效率极低——重复计算太多了。n=40时,函数调用次数已经上亿了。我建议你遇到这种场景,要么用迭代,要么用“记忆化递归”(把算过的结果存起来)。
11.4 函数指针
函数指针,就是指向函数的指针。你可以把它当作一个变量来传递、赋值、调用。嗯,这里要注意:函数名本身就是一个指针,指向函数的入口地址。
为什么需要函数指针?
- 实现回调机制(比如排序时传入比较函数)
- 实现多态(C语言没有面向对象,但函数指针可以模拟)
- 动态选择要调用的函数
#include <stdio.h>
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int main() {
int (*op)(int, int); // 声明一个函数指针
op = add;
printf("add: %d\n", op(10, 5)); // 15
op = sub;
printf("sub: %d\n", op(10, 5)); // 5
return 0;
}
你看,同一个函数指针op,可以指向不同的函数。这就是动态调度的雏形。
实用技巧:我经常用函数指针数组来实现“命令模式”。比如一个计算器程序,把加减乘除的函数指针放到数组里,用户输入操作符,直接通过索引调用。代码干净又高效。
int (*calc[])(int, int) = {add, sub, mul, div};
int result = calc[op_index](a, b);
知识体系总览
下面这张图,把本章的核心逻辑串起来了。你可以把它当作一个“思维导图”来看。
小结
这一章的内容,说白了就是让你对函数有更深的理解。局部变量和全局变量决定了数据的“可见范围”,存储类别让你能精细控制变量的生命周期。递归是一种优雅的思维方式,但要注意栈深度。函数指针则是C语言实现灵活设计的利器。
我个人觉得,函数指针是C语言里最被低估的特性。很多初学者觉得它难,就绕过去了。但如果你能熟练使用函数指针,写出来的代码会更有弹性,更容易扩展。我在做插件系统时,就是靠函数指针数组来实现动态加载不同算法的。
嗯,这一章就到这里。代码多敲几遍,尤其是递归和函数指针的例子,光看是学不会的。