当前位置: 首页 > news >正文

嵌入式 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;
}

运行结果如下:
请添加图片描述
执行时的栈空间分配过程:

  1. main函数运行时,栈中分配空间存放n=3fSP指针初始位置)。
  2. 第一次调用factorial(3)SP下移,分配栈空间存放factorial(3)的局部变量n=3
  3. factorial(3)调用factorial(2)SP继续下移,分配空间存放factorial(2)n=2
  4. 重复上述过程,直到调用factorial(0):此时已分配 4 层栈空间(factorial(3)factorial(0))。
    可见,即使计算n=3的阶乘,也需要 4 层栈空间。若n值过大(比如n=10000),栈空间会被持续占用,最终超过单片机有限的栈内存,导致栈溢出。因此,嵌入式开发中使用递归需格外谨慎,优先考虑循环替代(尤其在资源紧张的单片机上)。
🌐 变量的作用域:谁能访问这个变量?

变量的作用域指 “变量能被访问的范围”,主要分为局部变量和全局变量,理解它们的区别是写出可靠代码的基础。

局部变量 vs 全局变量
  • 局部变量:在函数内部定义的变量,仅在该函数内生效。若不同函数定义同名局部变量,它们会占用不同栈空间(是完全独立的变量)。
  • 全局变量:在所有函数外部定义的变量,可被同一文件内的所有函数访问。但需注意:编译器按顺序编译,若函数调用全局变量前未定义该变量,会报错(需先定义再使用)。
全局变量的跨文件使用

在多文件项目中(比如 STM32 工程的main.ctest.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=100test.cstatic 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 语言的更多细节,敬请期待~

http://www.dtcms.com/a/311500.html

相关文章:

  • 深入 Go 底层原理(十三):interface 的内部表示与动态派发
  • Javaweb————Apache Tomcat服务器介绍及Windows,Linux,MAC三种系统搭建Apache Tomcat
  • 技术文章:覆铜板的阻燃性
  • UniappDay07
  • 【AI】AIService(基本使用与指令定制)
  • cv快速input
  • 【云计算】云主机的亲和性策略(三):云主机 宿主机
  • Springboot原理和Maven高级
  • 操作系统:远程过程调用( Remote Procedure Call,RPC)
  • MQTT 入门教程:三步从 Docker 部署到 Java 客户端实现
  • Linux基础学习笔记二
  • MySQL PostgreSQL JDBC URL 配置允许批量操作
  • C语言输入安全10大边界漏洞解析与防御
  • 基于LSTM模型与加权链路预测的动态热门商品成长性分析
  • SpringBoot相关注解
  • 项目管理平台是什么?概念、定义、作用、主流厂商解读
  • docker:将python开发的大模型应用,打成docker容器
  • C#中的除法
  • PostGIS面试题及详细答案120道之 (081-090 )
  • cuda编程笔记(12)--学习cuFFT的简单使用
  • 【Mybatis】MyBatis分页的三种实现方式,Log4j的使用
  • Elasticsearch 混合检索一句 `retriever.rrf`,把语义召回与关键词召回融合到极致
  • 模拟激光相机工作站版本6.0 5.2.32 6.0.44 6.031 5.2.20
  • 题解:P4447 [AHOI2018初中组] 分组
  • 归并排序(简单讲解)
  • [论文阅读] 人工智能 + 软件工程 | GitHub Marketplace中CI Actions的功能冗余与演化规律研究
  • 【RK3568 看门狗驱动开发详解】
  • Kubernetes Gateway API 详解:现代流量路由管理方案
  • 【最后203篇系列】030 强化学习探索
  • 浏览器及java读取ros1的topic