嵌入式 C 语言入门:递归与变量作用域学习笔记 —— 从概念到内存特性
前言
大家好,这里是 Hello_Embed。上一篇笔记我们学习了函数的封装与参数传递,而函数的进阶用法中,递归和变量作用域是非常重要的概念 —— 递归能简化复杂问题的逻辑,而搞懂变量作用域(尤其是static
的用法)能帮我们避免嵌入式开发中常见的内存冲突和资源浪费。本文就来详细聊聊递归的原理、栈溢出的风险,以及变量作用域和static
关键字的核心知识点。
🔄 递归:函数调用自身的技巧
递归的基本概念
递归是指函数在执行过程中直接或间接调用自身的技术。最经典的例子是阶乘计算,代码如下:
int factorial(int n) {if(n == 0) { // 基例(停止条件)return 1;}return n * factorial(n - 1); // 调用自身
}
以factorial(5)
为例,递归过程可拆解为:
5 × factorial(4) → 5 × 4 × factorial(3) → 5 × 4 × 3 × factorial(2) → ... → 5×4×3×2×1×1
,最终得到结果 120。这种 “将大问题分解为同类小问题” 的思路,就是递归的核心。
递归的注意事项
递归虽简洁,但使用时有两个必须遵守的原则:
- 必须有基例(停止条件):如
n == 0
时返回 1,否则会陷入无限递归(函数永远调用自身,无法结束)。 - 警惕栈溢出:递归过程会持续分配栈空间,若层数过深,可能耗尽栈资源导致程序崩溃。
栈溢出现象:为什么递归可能 “撑爆” 内存?
结合上一篇笔记提到的 “栈” 概念,我们用阶乘函数的执行过程解释栈溢出,完整代码及运行结果:
#include <stdio.h>
int factorial(int n) {if(n == 0) {return 1;}return n * factorial(n - 1);
}
int main(void)
{int n = 3;int f = factorial(n);printf("f = %d\n\r", f); // 输出6return 0;
}
运行结果如下:
执行时的栈空间分配过程:
main
函数运行时,栈中分配空间存放n=3
和f
(SP
指针初始位置)。- 第一次调用
factorial(3)
:SP
下移,分配栈空间存放factorial(3)
的局部变量n=3
。 factorial(3)
调用factorial(2)
:SP
继续下移,分配空间存放factorial(2)
的n=2
。- 重复上述过程,直到调用
factorial(0)
:此时已分配 4 层栈空间(factorial(3)
到factorial(0)
)。
可见,即使计算n=3
的阶乘,也需要 4 层栈空间。若n
值过大(比如n=10000
),栈空间会被持续占用,最终超过单片机有限的栈内存,导致栈溢出。因此,嵌入式开发中使用递归需格外谨慎,优先考虑循环替代(尤其在资源紧张的单片机上)。
🌐 变量的作用域:谁能访问这个变量?
变量的作用域指 “变量能被访问的范围”,主要分为局部变量和全局变量,理解它们的区别是写出可靠代码的基础。
局部变量 vs 全局变量
- 局部变量:在函数内部定义的变量,仅在该函数内生效。若不同函数定义同名局部变量,它们会占用不同栈空间(是完全独立的变量)。
- 全局变量:在所有函数外部定义的变量,可被同一文件内的所有函数访问。但需注意:编译器按顺序编译,若函数调用全局变量前未定义该变量,会报错(需先定义再使用)。
全局变量的跨文件使用
在多文件项目中(比如 STM32 工程的main.c
和test.c
),全局变量可跨文件访问,但需用extern
声明:
- 在
main.c
中定义全局变量:int a = 0x55;
- 在
test.c
中使用该变量前,需声明:extern int a;
(告诉编译器 “a
在其他文件中定义”)。
全局变量的冲突与解决:用static
限制作用域
多人合作开发时,不同文件可能定义同名全局变量(比如甲在A.c
定义int time;
,乙在B.c
也定义int time;
),会导致编译器无法区分而报错。
解决办法:用static
修饰全局变量,将其作用域限制在当前.c
文件内:
A.c
中定义:static int time;
(仅A.c
内的函数可访问)B.c
中定义:static int time;
(仅B.c
内的函数可访问)
两者互不冲突,完美解决同名问题。
作用域优先级:“小范围” 优先
当不同作用域的变量同名时,作用域更小的变量优先生效。例如:
#include <stdio.h>
int a = 0x1111; // 全局变量(作用域:整个文件)
int mymain(void)
{int a = 0x55; // 局部变量(作用域:mymain函数内)printf("a = 0x%x\n\r", a); // 输出0x55(局部变量优先)return 0;
}
同理,若main.c
有全局变量n=100
,test.c
有static int n=50
,则test.c
中打印n
时,会优先使用作用域更小的static n=50
。
🧐 深入static
:静态变量的特性与区别
static
可修饰全局变量和局部变量,分别称为 “静态全局变量” 和 “静态局部变量”。它们与普通变量的核心区别,体现在内存特性上。
静态变量与全局变量的共性
静态变量(无论全局还是局部)和全局变量存放在全局数据区(而非栈区),因此有以下共性:
- 生命周期相同:从程序启动时创建,直到程序结束才释放(普通局部变量随函数调用结束而销毁)。
- 初始化特性相同:未手动初始化时,编译器会自动赋为
0
(普通局部变量未初始化时是随机值),如图:
- 存储位置一致:
都存放在全局数据区,而不是像普通局部变量那样存放在栈区。
静态全局变量 vs 静态局部变量的区别
类型 | 访问范围 | 核心特性 |
---|---|---|
静态全局变量 | 同一.c 文件内的所有函数均可访问 | 限制跨文件访问,避免全局变量冲突 |
静态局部变量 | 仅定义它的函数内部可访问 | 有 “记忆功能”:函数结束后不销毁,下次调用能保留上次的值 |
实例:用静态局部变量和全局变量统计递归次数
下面代码分别用静态局部变量和全局变量统计阶乘函数的调用次数,结果一致:
#include <stdio.h>
// 用静态局部变量统计
int factorial1(int n) {static int cnt1; // 静态局部变量,函数结束后不销毁cnt1 ++;if(n == 0) {printf("call back 1 %d ", cnt1); // 打印调用次数cnt1 = 0; // 重置计数return 1;}return n * factorial1(n - 1);
}// 用全局变量统计
int cnt2; // 全局变量
int factorial2(int n) {cnt2 ++;if(n == 0) {printf("call back 2 %d ", cnt2); // 打印调用次数cnt2 = 0; // 重置计数return 1;}return n * factorial2(n - 1);
}int mymain(void)
{factorial1(3); // 调用4次(3→2→1→0)factorial2(3); // 同样调用4次return 0;
}
可见,静态局部变量的 “记忆功能” 使其能替代全局变量完成计数,且避免了全局变量的作用域污染。
结尾
这篇笔记我们学习了递归的原理与风险(栈溢出),以及变量作用域的核心知识:局部变量与全局变量的区别、跨文件访问的extern
声明、用static
解决全局变量冲突,还深入理解了静态变量的内存特性。这些知识对嵌入式开发至关重要 —— 递归需谨慎使用以避免栈溢出,而合理管理变量作用域能减少内存冲突,提升代码可靠性。
下一篇笔记,我们将学习头文件的作用和多文件编程,看看如何用它们处理传感器数据或控制多个硬件设备。Hello_Embed 会继续和大家一起夯实基础,探索嵌入式 C 语言的更多细节,敬请期待~