credis 是一个"边造边学 C"的开源项目:用纯 C 从零手写一个迷你键值数据库。上一篇(第 0 章)搭了骨架、画了 C 知识体系地图;这一篇它第一次有用了。
先看效果
$ make run
credis 0.1.0 —— 输入 SET/GET/DEL/COUNT,QUIT 退出
credis> SET name kerro
OK
credis> GET name
kerro
credis> SET greeting hello world
OK
credis> DEL name
1
credis> GET name
(nil)
一个能存、能取、能删的内存键值库,交互像极简版 redis-cli。数据在内存里,退出就没——持久化是第 6 章的事。
代码从一个 main.c 长成了三个模块:
src/
├── main.c REPL:读一行 → 执行 → 打印 → 循环
├── command.c/.h 命令层:把 "SET foo bar" 解析成动作
└── store.c/.h 存储层:键值对怎么存、怎么找
分层不是仪式感:第 4 章把键盘换成 TCP 连接时,command 和 store 一行都不用改。
存储层:故意用最笨的方案
| |
一个 struct 打包一个键值对,128 个槽的定长数组就是整个数据库,查找从头到尾挨个 strcmp。
为什么不直接上哈希表?因为你还没有痛过。 第 3 章我们会实测这个线性查找在几千个键时有多慢,到那时哈希表才是"解药"而不是"黑话"。教学项目最大的失败就是把高级方案直接糊你脸上,跳过"它到底解决了什么"。
这一章真正的主角:C 字符串的坑,一次踩全
C 没有字符串类型,只有"以 \0 结尾的字符数组"。这一章的每个坑都源于此:
坑 1:strcmp 相等时返回 0。 if (strcmp(a, b)) 的意思是"不相等时"——新手最常写反的一行。
坑 2:容量要给 \0 留位置。 strlen 不数结尾符,所以边界检查是 strlen(key) >= 64,不是 > 64。
坑 3:拷贝函数三兄弟,两个是陷阱。
strcpy:无边界检查,缓冲区溢出之源,禁用strncpy:填满时不保证补\0,坑过无数人snprintf(dst, sizeof dst, "%s", src):永远以\0结尾,用这个
坑 4:读输入用 fgets,永远不要用 gets。 后者不检查边界,C11 已经把它从标准库里除名了——一个函数能被标准踢出去,可见闯了多大祸。
strtok:很"C"的野蛮美学
解析 SET greeting hello world 这行输入:
| |
strtok 不分配任何新内存——它直接在原字符串里把分隔符改写成 \0,返回每段的起始指针。所以入参不能是 const,而且解析完原字符串就面目全非了。
第一次见会觉得野蛮,但这正是 C 的风格:不隐藏成本。上层语言的 split() 每次都在堆上造一个新数组;strtok 一次分配都没有,代价是你必须理解它对内存做了什么。这个"必须理解"就是学 C 的收益本身。
C 式封装:.h 是承诺,.c 是实现,static 是门锁
| |
别的模块 #include "store.h" 就能用存储层,但想绕过接口直接摸 entries 数组?编译器直接拦住——static 在文件作用域的含义就是"外部不可见"。
C 没有 class,但封装的三要素齐全:接口(.h)、实现(.c)、访问控制(static)。第 3 章换哈希表时 store.h 几乎不变,调用方无感知——这就是封装换来的自由。
错误处理:返回值 + 调用方必须检查
| |
C 没有异常。store_set 用返回值报告结果,命令层 switch 逐一翻译成人话((error) store is full)。检查每一个可能失败的调用——这个肌肉记忆从第 1 章开始练,后面 malloc、socket、文件 IO 全靠它。这是"写过 C"和"学过 C 语法"的另一条分水岭。
试一试(包括把它弄坏)
| |
正常路径跑通后,故意打破它:SET 一个 100 字符的键、连续塞 129 个不同的键、只敲 SET 不带参数——错误路径也是行为的一部分,好软件的错误信息应该把话说清楚。
下一章预告
现在键和值的长度写死在编译期(64/256 字节),槽位只有 128 个——又浪费(短键也占满 64 字节)又僵硬(第 129 个键就存不进)。第 2 章引入 malloc/free:按实际内容分配内存,同时第一次直面 C 最著名的问题——这块内存,谁负责释放?
仓库在 github.com/KerroKapple/credis,每章一个 commit + 一篇 docs/ 教学文档。
