物联网不是“接个传感器”
我的专业叫物联网工程。课上会碰传感器、单片机、通信协议和云平台,听起来什么都要会一点。 大一我也这么理解,忙着把模块接亮,觉得灯亮了就是成功。
后来做课程作业才发现:灯亮只说明这一段通了。数据从节点到网关、再到服务端, 任何一段的边界没处理,系统就会在半夜掉线。传感器读到的是电压,程序要把它变成有意义的数; 网络收到的是字节,服务端要判断它是否完整。中间没有哪一步是“自动”的。
这个认识改变了我学 C 的方式。以前我把 C 当成考试语言,背语法、抄代码;
现在把它当成描述硬件的工具:一个 int 多大、指针指向哪里、缓冲区有没有越界、内存由谁释放。
物联网的代码离硬件近,错误不会只给我一个红色报错。它可能直接是死机、乱码, 或者一块板子再也连不上。所以我给自己定了个顺序:先能稳定跑,再能解释为什么,最后才是优化。
指针:地址、类型、边界
指针刚开始是我最没把握的部分。* 和 & 认识,题也能做,
但一写链表就不知道自己在改谁。后来我不再背“指针就是地址”,改成画三样东西:
变量、地址、箭头。
int value = 42;
int *p = &value; /* p 保存 value 的地址 */
int **pp = &p; /* pp 保存 p 的地址 */
printf("%d\n", *p); /* 42:顺着 p 找到 value */
printf("%p\n", (void *)p);
这段代码的价值不在输出,而在于我能手指着内存图说出每个 * 解了几层。
int *p 里的 * 是类型的一部分,*p 才是解引用。
它们长得一样,但不是同一件事。
另一个坎是空指针和野指针。指针没初始化、free 之后继续用、
函数返回局部数组的地址,这些错误可能当场崩,也可能安静地跑一会儿再崩。
我现在写链表会先问四个问题:
- 这个指针可能为空吗?为空时函数应该返回什么?
- 它指向的节点归谁释放?调用者还是被调用者?
- 改
next之前,后面那段链会不会丢? - 函数返回后,这个指针还指向有效内存吗?
链表、栈、队列:结构决定失败方式
链表是第一个让我觉得“数据结构不是图”的东西。图能看懂,代码却会在几个指针之间绕丢。 我后来固定用一个最小的单链表来练手:
typedef struct Node {
int value;
struct Node *next;
} Node;
Node *push_front(Node *head, int value)
{
Node *node = malloc(sizeof *node);
if (node == NULL) {
return head; /* 失败时不破坏原链表 */
}
node->value = value;
node->next = head;
return node;
}
void free_list(Node *head)
{
while (head != NULL) {
Node *next = head->next; /* 先保存,再释放 */
free(head);
head = next;
}
}
sizeof *node 比 sizeof(Node) 更抗类型改动。
但真正容易错的是失败路径和所有权:申请失败时原链表不能被破坏;
插入成功时旧 head 要接到新节点后面;释放时如果先 free 再读 next,就是使用已释放内存。
栈和队列看起来只是“后进先出”和“先进先出”,但实现方式会改变复杂度和失败模式。
数组栈要考虑扩容;链栈要为每个节点 malloc;循环队列最容易错在
head == tail 到底表示空还是满。
| 结构 | 核心规则 | 我的实现 | 最容易踩的坑 |
|---|---|---|---|
| 单链表 | 用 next 串联节点 | 不带头结点 | 丢链、重复释放、空表插入 |
| 栈 | LIFO,后进先出 | 动态数组 / 链栈 | 空栈 pop、扩容后指针失效 |
| 队列 | FIFO,先进先出 | 循环数组 / 链队列 | 判空判满、head 与 tail 更新 |
| 哈希表 | 平均 O(1) 查找 | 链地址法,学习中 | 扩容、哈希冲突、删除标记 |
复杂度表能背,但写的时候要能说出每一步为什么是 O(1) 或 O(n)。
比如循环队列,用额外一个 size 变量记录元素个数,判空判满就不再依赖“牺牲一个位置”的技巧,
代价只是多几个字节。工程里很多选择都是这种交换,不是标准答案。
调试比写代码更久,这很正常
我花在调试上的时间比写代码多。一开始会烦躁,后来接受了: 程序的行为和我脑子里的模型不一致,调试就是找出差异。
现在固定按这个顺序走:
- 先用
gcc -Wall -Wextra -Wpedantic编译,把警告当线索,不关掉。 - 加
-g -fsanitize=address,undefined,让越界和未定义行为尽早暴露。 - 把链表画在纸上,标出每一步的 head、tail 和 next。
- 写一个最小复现程序,把两百行缩成三十行,只留下出错的那条路径。
- 最后才开调试器,单步看变量的变化。
最有用的是画图。画完经常不用调试器就能发现问题:某个 next 被覆盖了,
或者 head 更新晚了一步。缩小问题范围的过程,本身就是在理解问题。
把网页放到公网:Cloudflare Pages 踩坑
把页面放到公网,是另一门课。我现在这套站点就是纯 HTML + CSS,没有构建步骤, 直接传到 Cloudflare Pages。它适合我这种静态记录:快、便宜、坏了好查。
目前踩过或差点踩到的坑:
- 入口文件必须是
index.html。文件名大小写在本地不敏感,线上可能敏感,部署后就会 404。 - 路径尽量用相对链接。
href="cs.html"比根路径更适合项目页,也方便以后换自定义域。 - 别把源文件一起传上去。根目录混入不该公开的笔记、压缩包或环境配置,迟早会变成问题。
- 自定义域一次只改一个变量。DNS 解析、代理状态、SSL 模式同时动,出问题就不知道是谁的锅。
- 更新后先强刷。浏览器和边缘缓存都可能留着旧页面,不要第一时间怀疑部署失败。
- 逐页点一遍。主页正常不代表子页正常,尤其检查导航、返回链接和图片路径。
域名是另一层。买域名、改 nameserver、等解析生效、确认 HTTPS,每一步都能卡住。
我的经验是:一次只改一个变量,改完用 dig 或在线 DNS 工具确认,再动下一步。
别在凌晨同时改 DNS、SSL 和缓存,否则出问题只能靠猜。
下一阶段
接下来想把基础打得更实一点,不急着追新框架:
- 手写单链表、双向链表、循环队列,并补上边界测试。
- 用 C 做一个小型的传感器数据缓冲区,把“生产者 / 消费者”真正跑起来。
- 把课上的数据结构全部自己实现一遍,不只抄示例。
- 给站点补 404 页面和更清楚的部署记录。
我不想把“学过”写成“掌握”。能独立复现、能解释边界、能修好自己写崩的程序,才算往前一格。