局部变量、静态变量(全局变量)和动态变量的区别

我不明白了,到一个函数内部定义的变量,会在函数结束时销毁,是吧?static说是静态变量修饰符,这是什么意思呢?你前面说多个任务同时调用会重写同一片内存又是怎么回事?还有malloc是什么性质的呢?

这三个问题直指 C 语言内存管理的核心,也是理解 RTOS 任务安全性的基石。我们把内存想象成一个大仓库,不同的修饰符决定了数据存放在仓库的哪个“房间”。


1. 局部变量:函数结束就销毁

你在函数内部定义的普通变量(如 uint8_t keyNum = 0;),在 C 语言中叫自动变量(Stack Variable)

  • 存放位置:存放在该任务的**栈(Stack)**空间里。

  • 生命周期:当函数执行到 } 结束时,这块内存就会被回收,给下一个函数使用。

  • 安全性:它是线程安全的。因为 FreeRTOS 会给每个任务分配独立的栈,任务 A 调用函数时用的栈和任务 B 调用时用的栈互不干扰。


2. static 静态变量:仓库里的“固定车位”

当你加了 static,变量的性质就完全变了。

  • 存放位置:存放在静态数据区(Global/Static RAM),不再属于某个任务的栈。

  • 生命周期:从程序启动到关机,它永远存在,不会因为函数结束而销毁。它会记住上一次被修改后的值。

  • “重写同一片内存”是怎么回事?

    • 你的 Ming_print 之前定义了 static char print_buffer[128]

    • 这意味着,无论 Key_Task 还是 LED_Task 调用这个函数,它们操作的都是物理地址完全相同的那 128 字节。

    • 冲突场景Key_Task 正在往这 128 字节里写 “Key Pressed”,写到一半,LED_Task 抢占了 CPU,也往这同一个地址写 “LED Toggle”。等 Key_Task 恢复运行去发送串口时,buffer 里的内容已经变成了“半驴半马”的乱码。


3. malloc (pvPortMalloc):按需申请的“临时工”

malloc 是动态内存分配,在 FreeRTOS 中我们推荐使用 pvPortMalloc

  • 存放位置:存放在**堆(Heap)**空间。

  • 性质:它像是在酒店“开房”。你调用它,系统给你一张房卡(指针),这间房就归你专用了,别人不能进。

  • 为什么它能解决日志冲突?

    • 每个任务调用 Ming_print 时,都会独立申请一块属于自己的 128 字节。

    • 任务 A 拿到的地址是 0x20001000,任务 B 拿到的地址是 0x20001080

    • 它们往各自的地址写东西,互不干扰。

  • 风险:房间开完必须退房!如果你不调用 vPortFree,堆内存很快就会被占满,这就是“内存泄漏”。


总结对比表

类型 关键字 存放房间 销毁时机 多任务安全吗?
局部变量 栈 (Stack) 函数执行完即销毁 安全(每个任务有独立的栈)
静态变量 static 静态区 (RAM) 永不销毁 不安全(所有任务共用一个地址)
动态内存 malloc 堆 (Heap) 手动释放时销毁 安全(只要每个任务申请独立的地址)

回到你的日志改造

之所以让你在 Ming_print 里用 pvPortMalloc

  1. 任务 A 申请了内存块 1,写好日志,把指针 1 丢进队列。

  2. 任务 B 抢占,申请了内存块 2,写好日志,把指针 2 丢进队列。

  3. Log_Task 依次从队列拿到指针 1 和指针 2,打印完后,分别把它们 free 掉。

这就是通过“动态内存 + 队列”实现的异步解耦。你想先尝试在你的 MingLog.c 里引入 pvPortMalloc 吗?(注意:需要包含 FreeRTOS.htask.h 才能使用它)

俺的总结:
static变量初始化只在系统第一次遇到时执行,后面那句初始化指令就隐形了是吧?但局部变量和动态内存的初始化都不会隐形;他们间的另一个重要区别就是销毁的时机,有自动、手动、永续三种,是吧?动态内存无论在何处申请了,只要不free就一直存在。

你的理解非常精准,看来你已经摸到了 C 语言底层内存管理的门径。我们来针对性地确认这三个核心认知:

1. 关于 static 变量的“隐形”初始化

你的理解是完全正确的。

  • 执行机制:对于局部静态变量(函数内部定义的 static),编译器会生成一段特殊的代码,确保初始化逻辑只运行一次

  • 后续行为:当程序第二次、第三次进入该函数时,那句初始化语句在物理上确实“隐形”了。程序会直接跳过它,去静态存储区读取上一次留下的值。

  • 注意:在嵌入式中,这些值通常存放在 .data(有初值)或 .bss(初值为 0)段,由启动代码(Startup code)在进入 main 函数前加载。

2. 三种销毁时机(自动、手动、永续)

你总结的这三种分类非常经典,正好对应了 C 语言的三大存储区:

销毁类型 对应存储区 描述
自动销毁 栈 (Stack) 函数执行到 },内存瞬间释放。这就是局部变量,它的生存期和函数调用绑定。
手动销毁 堆 (Heap) 必须通过 free()vPortFree() 显式归还。这就是你提到的动态内存。
永续存在 静态区 (RAM) 程序不关机,它就不消失。包括全局变量和 static 变量。

3. 动态内存的“顽固性”

没错,它是最危险也最强大的工具。

  • 生命周期:动态内存(Heap)是唯一一个跨越函数边界跨越任务边界的存储方式。

  • 一直存在:正如你所说,无论你在哪个角落申请了内存,只要不 free,哪怕申请它的那个函数早就结束了,这块内存依然处于“被占用”状态。

  • 后果:如果你在日志函数里频繁申请而不释放,这叫内存泄漏 (Memory Leak)。在单片机上,由于内存极小,可能运行几小时后 pvPortMalloc 就会返回 NULL,导致系统打印不出日志甚至直接崩溃。


结合你的日志任务 (Log Task) 再看流程:

这三种内存会在你的日志系统中协同作战

  1. 生产者任务(比如 Key Task):调用 Ming_print

  2. 动态内存 (pvPortMalloc):在堆里“手动”申请 128 字节,把日志填进去。

  3. 队列 (Queue):把这个申请到的“房卡”(地址/指针)传给 Log 任务。

  4. 消费者任务(Log Task):拿到“房卡”,去堆里读取日志并发送串口。

  5. 手动销毁 (vPortFree):Log 任务最后执行释放。这块内存才重新变回可用状态。


一个必须要提醒你的“陷阱”:

你之前定义的 static char print_buffer[128] 属于永续存在。

如果多个任务同时写这个 buffer,虽然它不会销毁,但它会被**“反复横跳”地覆盖**。就像一块黑板,Key 任务刚写完,LED 任务冲上来擦了写上自己的,最后串口发出去的可能就是两者的混合体。

这就是为什么在 多任务并行 的环境下,我们必须抛弃局部 static 缓冲区,转而使用**局部变量(栈)动态内存(堆)**的原因。

既然理解了内存模型,你现在敢尝试在 Ming_print 里写下那句 char *pcMsg = (char *)pvPortMalloc(128); 吗?(记得在 Log_Task 那边要对应的 vPortFree 哦!)


局部变量、静态变量(全局变量)和动态变量的区别
https://mingzaitown.github.io/2026/01/18/STM32/局部变量、静态变量(全局变量)和动态变量的区别/
作者
MingZai
发布于
2026年1月19日
许可协议