学习笔记 · Obsidian
C 指针、数组和内存:从地址到生命周期
一个指针不能回答所有问题
uint16_t value = 42u;
uint16_t *p = &value;
*p = 43u;
value 是对象,&value 是它的地址,p 保存指向它的指针值,*p 访问目标对象。不要把“指针变量本身”和“指向的数据”混成一件事。p + 1 按一个 uint16_t 元素的跨度计算地址,不是笼统加一个字节。GNU Pointers
p(指针变量) ───指向───> value(uint16_t 对象,当前值 43)
&p 是 p 自己的地址;它不是 &value。
看见任何指针参数,先补齐四项契约:指向的类型、有效长度、可读写性、有效时间。再问由谁拥有和释放这段存储。
数组和指针有关,但不是同一类型
uint16_t a[4] = {10u, 20u, 30u, 40u};
uint16_t *p = a;
size_t count = sizeof a / sizeof a[0];
a 是四个元素的数组,p 是一个指针。许多表达式会把数组转换为首元素指针;sizeof a 则仍得到整个数组的字节数。GNU Arrays
函数参数写成 uint16_t a[] 时会调整为指针参数,在函数内部再计算 sizeof a / sizeof a[0] 得不到调用方数组长度。因此第 01 课显式传入 count。SEI CERT ARR01-C
用索引时保持 i < count。尾后地址可用于比较或计算,不能解引用;非空指针也可能已经悬空。检查 p != NULL 只是必要条件之一。
生命周期比地址更关键
| 存储方式 | 何时有效 | 嵌入式中的典型错误 |
|---|---|---|
| 普通局部对象 | 所属执行块有效期间 | 把它的地址交给稍后执行的回调 |
| 静态存储期对象 | 程序执行期间 | 多任务改同一对象却没有同步 |
| 动态分配对象 | 分配成功至释放 | 泄漏、重复释放、释放后使用 |
| 驱动借出的缓冲区 | 由驱动契约决定 | 回调结束后仍保存并使用地址 |
这些对象的有效期遵循 C 存储期规则;“栈、堆、静态区”是常见实现模型,不能用它代替语言规则。N1570 §6.2.4
本学习项目优先使用有明确上限的数组和结构体。需要 malloc 时,约定失败处理、唯一释放者以及跨任务交接方式。释放一个指针后把它置空,也不会自动修复其他指向相同对象的别名。
const 从右往左看
| 声明 | 哪部分不能通过这个声明改变 |
|---|---|
const uint8_t *p | 指向的数据 |
uint8_t * const p = buf | 指针变量的目标地址 |
const uint8_t * const p = buf | 两者 |
第一种仍可能存在其他可写别名;const 不会让整块存储自动成为跨线程不可变数据。
网络数据为什么不能直接 printf
UART、HTTP、MQTT 传入的通常是“缓冲区 + 长度”,未必有结尾 \0。对这种数据直接用 %s、strlen 会越过有效范围。若转为字符串,需要另给结尾预留一个字节,并限制最大长度;二进制内容还可能在中间包含零字节。ESP-MQTT 事件结构
练习与判定
给第 01 课的 summarize 画出调用方数组、函数参数、输出结构体三者关系。然后解释:“把传入的 count 改成 100,为什么只检查非空不能保护程序?”
- 参考答案 地址不携带数组长度。当前函数会先因
count > MAX_SAMPLES拒绝 100,但如果实际数组只有 2 个元素而传入 8,仍违反契约,可能越界。上限检查和实际可访问长度是两件事。
过关标准:能独立设计“指针 + 长度 + 输出结果 + 错误返回值”的接口,并说明缓冲区可以活到什么时候。