memset 清不净——sizeof(指针) 只清 4 字节,传真实结构体大小
文章标签:#嵌入式开发 #单片机 #C语言 #野指针 #内存越界
摘要:本文从一次刷卡门禁的"串号"事故切入,剖析 memset 清零结构体时只清了 4 字节的经典陷阱。核心在于 sizeof(指针) 永远是指针宽度而非对象大小——32 位机恒为 4。文章从指针本质讲起,对比 sizeof(p)、sizeof(*p)、sizeof(类型) 三种写法,用表格和决策图帮你快速判断,并附错误复现与正确代码实测对比,助你彻底避开"清了个寂寞"的坑。
一、开篇:那个"清了跟没清一样"的凌晨
去年做一款刷卡门禁控制器,每次重新配置用户参数前我会调用 memset(&user, 0, sizeof(user)) 把结构体清零。本地自测一切正常,可现场反馈:偶尔新录入的用户会"继承"上一个人的部分卡片号,导致刷错卡开门。复现率约千分之二,且只在连续快速换人录入时出现。
如果你写过"先清空再赋值"的代码,这一讲能帮你彻底避开 memset 只清了 4 字节这个经典陷阱。我会从指针本质讲起,给你一套不会翻车的清空套路,并附上对比实测。
全文导航:入门认知(指针与 sizeof 的本质)→ 进阶原理(为什么只清 4 字节)→ 高级应用(错误写法复现 + 正确清空代码 + 实测对比)。
二、入门认知:memset 到底在清什么
memset 的原型是 void *memset(void *s, int c, size_t n),第三个参数是"要清的字节数"。陷阱就在这个 n——很多人习惯写 sizeof(ptr),但 ptr 是指针变量时,sizeof(ptr) 是"指针本身的字节数"(32 位机 4、64 位机 8),而不是它指向的结构体大小。
类比:你让保洁阿姨"把那个文件袋清空",结果你递给她的是"写着文件袋地址的便签纸",她只把便签纸擦干净了,文件袋纹丝不动。
如图 1 展示指针变量与所指向结构体在内存中的关系:
#mermaid-svg-QYzgcrKUvo47Hq30{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-QYzgcrKUvo47Hq30 .error-icon{fill:#552222;}#mermaid-svg-QYzgcrKUvo47Hq30 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-QYzgcrKUvo47Hq30 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-QYzgcrKUvo47Hq30 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-QYzgcrKUvo47Hq30 .marker.cross{stroke:#333333;}#mermaid-svg-QYzgcrKUvo47Hq30 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-QYzgcrKUvo47Hq30 p{margin:0;}#mermaid-svg-QYzgcrKUvo47Hq30 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-QYzgcrKUvo47Hq30 .cluster-label text{fill:#333;}#mermaid-svg-QYzgcrKUvo47Hq30 .cluster-label span{color:#333;}#mermaid-svg-QYzgcrKUvo47Hq30 .cluster-label span p{background-color:transparent;}#mermaid-svg-QYzgcrKUvo47Hq30 .label text,#mermaid-svg-QYzgcrKUvo47Hq30 span{fill:#333;color:#333;}#mermaid-svg-QYzgcrKUvo47Hq30 .node rect,#mermaid-svg-QYzgcrKUvo47Hq30 .node circle,#mermaid-svg-QYzgcrKUvo47Hq30 .node ellipse,#mermaid-svg-QYzgcrKUvo47Hq30 .node polygon,#mermaid-svg-QYzgcrKUvo47Hq30 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-QYzgcrKUvo47Hq30 .rough-node .label text,#mermaid-svg-QYzgcrKUvo47Hq30 .node .label text,#mermaid-svg-QYzgcrKUvo47Hq30 .image-shape .label,#mermaid-svg-QYzgcrKUvo47Hq30 .icon-shape .label{text-anchor:middle;}#mermaid-svg-QYzgcrKUvo47Hq30 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-QYzgcrKUvo47Hq30 .rough-node .label,#mermaid-svg-QYzgcrKUvo47Hq30 .node .label,#mermaid-svg-QYzgcrKUvo47Hq30 .image-shape .label,#mermaid-svg-QYzgcrKUvo47Hq30 .icon-shape .label{text-align:center;}#mermaid-svg-QYzgcrKUvo47Hq30 .node.clickable{cursor:pointer;}#mermaid-svg-QYzgcrKUvo47Hq30 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-QYzgcrKUvo47Hq30 .arrowheadPath{fill:#333333;}#mermaid-svg-QYzgcrKUvo47Hq30 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-QYzgcrKUvo47Hq30 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-QYzgcrKUvo47Hq30 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QYzgcrKUvo47Hq30 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-QYzgcrKUvo47Hq30 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QYzgcrKUvo47Hq30 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-QYzgcrKUvo47Hq30 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-QYzgcrKUvo47Hq30 .cluster text{fill:#333;}#mermaid-svg-QYzgcrKUvo47Hq30 .cluster span{color:#333;}#mermaid-svg-QYzgcrKUvo47Hq30 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-QYzgcrKUvo47Hq30 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-QYzgcrKUvo47Hq30 rect.text{fill:none;stroke-width:0;}#mermaid-svg-QYzgcrKUvo47Hq30 .icon-shape,#mermaid-svg-QYzgcrKUvo47Hq30 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-QYzgcrKUvo47Hq30 .icon-shape p,#mermaid-svg-QYzgcrKUvo47Hq30 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-QYzgcrKUvo47Hq30 .icon-shape .label rect,#mermaid-svg-QYzgcrKUvo47Hq30 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-QYzgcrKUvo47Hq30 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-QYzgcrKUvo47Hq30 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-QYzgcrKUvo47Hq30 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
指向
只清了这 4 字节
清整个结构体
指针变量 ptr(占 4 字节, 存地址 0x20001000)
结构体 user(占 64 字节)
memset(ptr,0,sizeof(ptr))
memset(ptr,0,sizeof(*ptr))
图 1:指针变量与所指向对象的内存关系,sizeof(指针)≠sizeof(对象)
适用边界:memset 适合清零/填充连续的裸内存;如果结构体含指针成员(如 char *name),memset 只能清掉指针值,不会释放它指向的堆内存,那属于另一类泄漏问题。
三、进阶原理:为什么只清了 4 字节
在 32 位 MCU(如 STM32F1/F4)上,任何指针变量 T *p 的 sizeof(p) 恒为 4。当你写下:
memset(p, 0, sizeof(p)); // 实际只清 4 字节 —— p 自己的值被置 0 了!
更糟的是,这里把指针本身清成了 NULL,后续若再用 p 访问结构体,直接硬 Fault。
而正确的两种写法:
memset(p, 0, sizeof(*p)); // 解引用取对象,得到真实结构体大小
memset(p, 0, sizeof(StructType)); // 用类型名,安全但需保证类型一致
易混淆点用表格对比:
| sizeof(p) | 4 | 仅指针变量自身 | 结构体没清,指针变 NULL |
| sizeof(*p) | 结构体大小 | 整个结构体 | 正确清零 |
| sizeof(StructType) | 结构体大小 | 整个结构体 | 正确,但类型名易写错 |
[坑] 高频翻车点:把"数组名"和"指针"记混。数组名 arr 的 sizeof(arr) 是数组总字节数(正确),但函数参数里的 void *buf 退化成指针,sizeof(buf) 又变 4。一维数组传参进函数后同样会踩坑。
四、高级应用:复现与根治
错误写法演示
typedef struct {
uint32_t card_id[2];
uint8_t name[16];
uint8_t level;
} User_t; // 共 8+16+1 = 25 字节(含对齐约 28)
void reload_user(User_t *p)
{
memset(p, 0, sizeof(p)); // 错误:只清 4 字节,p 自身变 NULL
p->card_id[0] = read_card(); // 此时 p 已是 NULL,访问即 HardFault
}
逐行推演:p 是指针,sizeof(p)=4,memset 把 p 指向的"指针变量的 4 字节存储位置"清零——注意 p 是局部形参,清的是栈上那份 p 副本,原函数里调用方的指针不受影响,但 p 在函数内变 NULL,下一行解引用直接崩。
正确完整代码
#include <string.h>
typedef struct {
uint32_t card_id[2];
uint8_t name[16];
uint8_t level;
} User_t;
/* 写法一:解引用,推荐,类型随 p 自动跟随 */
void reload_user_safe(User_t *p)
{
if (p == NULL) return;
memset(p, 0, sizeof(*p)); // 清整个结构体
p->card_id[0] = read_card();
p->level = 1;
}
/* 写法二:传结构体本身(避免指针歧义) */
void clear_user(User_t *p)
{
if (p) memset(p, 0, sizeof(User_t));
}
改前 vs 改后实测对比
| 清零字节数(结构体28B) | 4 | 28 |
| 连续换人录入串号概率 | 千分之二 | 0 |
| 是否 HardFault | 是(指针变 NULL) | 否 |
[提示] 如果结构体含柔性数组或动态指针成员,memset 后需单独重新赋值指针/重新分配,切勿以为"全清了就安全"。
下面这张决策图帮你快速判断该用哪种清零写法:
#mermaid-svg-PGpiZkr5mtlUexQs{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-PGpiZkr5mtlUexQs .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-PGpiZkr5mtlUexQs .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-PGpiZkr5mtlUexQs .error-icon{fill:#552222;}#mermaid-svg-PGpiZkr5mtlUexQs .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-PGpiZkr5mtlUexQs .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-PGpiZkr5mtlUexQs .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-PGpiZkr5mtlUexQs .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-PGpiZkr5mtlUexQs .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-PGpiZkr5mtlUexQs .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-PGpiZkr5mtlUexQs .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-PGpiZkr5mtlUexQs .marker{fill:#333333;stroke:#333333;}#mermaid-svg-PGpiZkr5mtlUexQs .marker.cross{stroke:#333333;}#mermaid-svg-PGpiZkr5mtlUexQs svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-PGpiZkr5mtlUexQs p{margin:0;}#mermaid-svg-PGpiZkr5mtlUexQs .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-PGpiZkr5mtlUexQs .cluster-label text{fill:#333;}#mermaid-svg-PGpiZkr5mtlUexQs .cluster-label span{color:#333;}#mermaid-svg-PGpiZkr5mtlUexQs .cluster-label span p{background-color:transparent;}#mermaid-svg-PGpiZkr5mtlUexQs .label text,#mermaid-svg-PGpiZkr5mtlUexQs span{fill:#333;color:#333;}#mermaid-svg-PGpiZkr5mtlUexQs .node rect,#mermaid-svg-PGpiZkr5mtlUexQs .node circle,#mermaid-svg-PGpiZkr5mtlUexQs .node ellipse,#mermaid-svg-PGpiZkr5mtlUexQs .node polygon,#mermaid-svg-PGpiZkr5mtlUexQs .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-PGpiZkr5mtlUexQs .rough-node .label text,#mermaid-svg-PGpiZkr5mtlUexQs .node .label text,#mermaid-svg-PGpiZkr5mtlUexQs .image-shape .label,#mermaid-svg-PGpiZkr5mtlUexQs .icon-shape .label{text-anchor:middle;}#mermaid-svg-PGpiZkr5mtlUexQs .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-PGpiZkr5mtlUexQs .rough-node .label,#mermaid-svg-PGpiZkr5mtlUexQs .node .label,#mermaid-svg-PGpiZkr5mtlUexQs .image-shape .label,#mermaid-svg-PGpiZkr5mtlUexQs .icon-shape .label{text-align:center;}#mermaid-svg-PGpiZkr5mtlUexQs .node.clickable{cursor:pointer;}#mermaid-svg-PGpiZkr5mtlUexQs .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-PGpiZkr5mtlUexQs .arrowheadPath{fill:#333333;}#mermaid-svg-PGpiZkr5mtlUexQs .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-PGpiZkr5mtlUexQs .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-PGpiZkr5mtlUexQs .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PGpiZkr5mtlUexQs .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-PGpiZkr5mtlUexQs .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PGpiZkr5mtlUexQs .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-PGpiZkr5mtlUexQs .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-PGpiZkr5mtlUexQs .cluster text{fill:#333;}#mermaid-svg-PGpiZkr5mtlUexQs .cluster span{color:#333;}#mermaid-svg-PGpiZkr5mtlUexQs div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-PGpiZkr5mtlUexQs .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-PGpiZkr5mtlUexQs rect.text{fill:none;stroke-width:0;}#mermaid-svg-PGpiZkr5mtlUexQs .icon-shape,#mermaid-svg-PGpiZkr5mtlUexQs .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-PGpiZkr5mtlUexQs .icon-shape p,#mermaid-svg-PGpiZkr5mtlUexQs .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-PGpiZkr5mtlUexQs .icon-shape .label rect,#mermaid-svg-PGpiZkr5mtlUexQs .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-PGpiZkr5mtlUexQs .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-PGpiZkr5mtlUexQs .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-PGpiZkr5mtlUexQs :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
数组名
指针
是, 已退化成指针
否, 局部指针
要清零的对象是什么?
是数组名还是指针?
sizeof(arr) 是数组总字节数可直接用
在函数参数里?
sizeof(buf) 恒为 4/8需单独传 size 参数
用 sizeof(*p) 解引用得到真实结构体大小
正确清零
图 4:清零写法决策流程图
五、收尾:要点复盘与最佳实践
核心要点:① sizeof(指针) 永远是指针宽度,不是对象大小;② 清零对象用 sizeof(*p) 或 sizeof(类型);③ 数组传参退化成指针后 sizeof 失效,函数内应额外传入长度。
最佳实践清单(可收藏):
- 清零对象统一用 memset(p, 0, sizeof(*p)),类型自动跟随最安全。
- 函数内清数组,一定要单独传 size 参数,别信 sizeof(buf)。
- 结构体含指针成员时,memset 不清堆内存,需单独处理。
- 把 sizeof(p) 当字节数用前,先问自己"p 是指针还是数组名"。
- 关键清零逻辑加静态断言:_Static_assert(sizeof(*p)==预期, "大小不符")。
进阶延伸:C11 的 memset_s 可防止被编译器优化掉(敏感数据擦除场景必用);想整块清零大缓冲区,DMA 清内存比 CPU 循环更快。
互动:你踩过 sizeof(指针) 的坑吗?评论区聊聊你见过的"清了个寂寞"现场。




