---
type: study
status: active
created: 2026-09-10
updated: 2026-09-10
sensitivity: private
tags: [study, embedded, c, esp32, cores3]
sources:
  - "https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Pointers.html"
  - "https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Arrays.html"
  - "https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf"
---
# C 指针、数组和内存：从地址到生命周期

## 一个指针不能回答所有问题

```c
uint16_t value = 42u;
uint16_t *p = &value;
*p = 43u;
```

`value` 是对象，`&value` 是它的地址，`p` 保存指向它的指针值，`*p` 访问目标对象。不要把“指针变量本身”和“指向的数据”混成一件事。`p + 1` 按一个 `uint16_t` 元素的跨度计算地址，不是笼统加一个字节。[GNU Pointers](https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Pointers.html)

```text
p（指针变量） ───指向───> value（uint16_t 对象，当前值 43）
&p 是 p 自己的地址；它不是 &value。
```

看见任何指针参数，先补齐四项契约：指向的类型、有效长度、可读写性、有效时间。再问由谁拥有和释放这段存储。

## 数组和指针有关，但不是同一类型

```c
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](https://www.gnu.org/software/c-intro-and-ref/manual/html_node/Arrays.html)

函数参数写成 `uint16_t a[]` 时会调整为指针参数，在函数内部再计算 `sizeof a / sizeof a[0]` 得不到调用方数组长度。因此第 01 课显式传入 `count`。[SEI CERT ARR01-C](https://wiki.sei.cmu.edu/confluence/display/c/ARR01-C.%2BDo%2Bnot%2Bapply%2Bthe%2Bsizeof%2Boperator%2Bto%2Ba%2Bpointer%2Bwhen%2Btaking%2Bthe%2Bsize%2Bof%2Ban%2Barray)

用索引时保持 `i < count`。尾后地址可用于比较或计算，不能解引用；非空指针也可能已经悬空。检查 `p != NULL` 只是必要条件之一。

## 生命周期比地址更关键

| 存储方式 | 何时有效 | 嵌入式中的典型错误 |
| --- | --- | --- |
| 普通局部对象 | 所属执行块有效期间 | 把它的地址交给稍后执行的回调 |
| 静态存储期对象 | 程序执行期间 | 多任务改同一对象却没有同步 |
| 动态分配对象 | 分配成功至释放 | 泄漏、重复释放、释放后使用 |
| 驱动借出的缓冲区 | 由驱动契约决定 | 回调结束后仍保存并使用地址 |

这些对象的有效期遵循 C 存储期规则；“栈、堆、静态区”是常见实现模型，不能用它代替语言规则。[N1570 §6.2.4](https://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf)

本学习项目优先使用有明确上限的数组和结构体。需要 `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 事件结构](https://docs.espressif.com/projects/esp-idf/en/v5.5.1/esp32s3/api-reference/protocols/mqtt.html)

## 练习与判定

给第 01 课的 `summarize` 画出调用方数组、函数参数、输出结构体三者关系。然后解释：“把传入的 `count` 改成 100，为什么只检查非空不能保护程序？”

> [!question]- 参考答案
> 地址不携带数组长度。当前函数会先因 `count > MAX_SAMPLES` 拒绝 100，但如果实际数组只有 2 个元素而传入 8，仍违反契约，可能越界。上限检查和实际可访问长度是两件事。

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

---

[[Study/embedded-cores3/CoreS3学习入口|返回学习入口]] · [[Study/embedded-cores3/CoreS3资料来源与版本|来源与版本]]
