学习笔记 · Obsidian

C 指针、数组和内存:从地址到生命周期

ESP-IDFC / 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,仍违反契约,可能越界。上限检查和实际可访问长度是两件事。

过关标准:能独立设计“指针 + 长度 + 输出结果 + 错误返回值”的接口,并说明缓冲区可以活到什么时候。


返回学习入口 · 来源与版本