一个用纯 C 写的控制台图书管理系统:管理员管理用户和图书,读者登录后借书、还书、查看自己的借阅记录。
没有第三方库,只有标准库,git clone 下来就能编译运行。
项目规模:28 个源文件(inc/ + src/)/ 2000+ 行代码——比课本上"单文件 200 行"的作业大得多,
又比真实项目小得多,正好卡在"能看懂、但一个人写会乱"的区间,是练多文件工程的最佳尺寸。
| 你会在这个项目里遇到 | 对应到哪里看 |
|---|---|
.h 和 .c 为什么分开、怎么分 |
inc/ 与 src/ 一一对应,接口放头文件 |
| 一个字段要贯穿五个环节 | 见 练习任务 第 2 题 |
| 动态数组(顺序表)与扩容 | src/user_list.c 的 realloc |
| 数据怎么持久化到文件 | src/*_file.c,逗号分隔的纯文本 |
| 界面 / 数据 / 存储怎么分层 | 阅读指南 的分层图 |
| 边界情况怎么处理 | read_int / input_ended,以及下面"踩坑记录" |
| Git 日常操作 | 如何用 Git 学习 |
- VS Code → 文件 → 打开文件夹 → 选择本项目目录
- 按
F5:编译 + 启动调试(可以打断点单步走) - 按
Ctrl+F5:编译 + 直接运行 - 按
Cmd+Shift+B/Ctrl+Shift+B:只编译
.vscode/tasks.json里默认用的是/usr/bin/clang(macOS)。 Windows 用户把其中的command改成gcc、把-o后面的文件名加个.exe即可。
# macOS / Linux
cc -g -Wall -Wextra -I inc src/*.c -o bin/library && ./bin/library
# Windows(MinGW / Git Bash)
gcc -g -Wall -Wextra -I inc src/*.c -o bin/library.exe && ./bin/library.exemake # 编译,生成 bin/library
make run # 编译并运行
make clean # 清理编译产物
⚠️ 必须在项目根目录运行程序:数据文件用的是相对路径data/user.data, 换到别的目录运行会新建一份空数据。
| 身份 | 账号 | 密码 |
|---|---|---|
| 管理员 | admin |
admin123(在 src/admin.c 顶部修改) |
| 读者 | zhangsan |
111111 |
| 读者 | lisi |
222222 |
| 读者 | wangwu |
333333 |
功能菜单:1.管理员登录 2.用户登录 0.退出系统
管理员功能:1.用户管理 2.图书管理 3.借阅记录查询 0.退出管理员登录
用户管理:1.添加用户 2.删除用户 3.查询用户列表 4.修改用户 5.按ID查询 6.按姓名查询 0.退出
图书管理:1.添加图书 2.删除图书 3.查询图书列表 4.修改图书 5.按ID查询 6.按书名查询 0.退出
借阅记录:1.查看全部借阅记录 2.按用户查询 3.按图书查询 4.查看未归还记录 0.退出
用户功能:1.查询图书 2.借书 3.还书 4.我的借阅记录 0.退出登录
借书会扣库存并生成借阅记录,还书会加回库存并把记录标记为已归还——内存和文件同时更新。
.
├─ .vscode/ # VS Code 编译与调试配置(不用改)
├─ inc/ # 头文件:模块对外提供的接口
├─ src/ # 源文件:具体实现
├─ data/ # 数据文件(纯文本,逗号分隔)
├─ docs/ # 阅读指南 / 练习任务 / Git 教程
├─ Makefile
└─ README.md
源码按职责分三层,看文件名就能认出来:
| 文件 | 是什么 |
|---|---|
main.c / admin*.c / user.c |
界面层:打印菜单、读键盘 |
*_list.c |
数据层:内存里的动态数组 |
*_file.c |
持久化层:内存 ↔ 文件 |
view.c / util.c / common.h |
表格打印 / 工具函数 / 全局常量 |
详细的调用关系见 阅读指南。
- 先跑起来:编译、运行,把菜单每个功能都点一遍,建立直观印象
- 再读代码:跟着 阅读指南 走三条主线(启动 / 借书 / 添加图书),
在
src/user.c的user_borrow_book()打个断点,用F10/F11单步走一遍 - 然后改代码:从 练习任务 的一档开始,每做完一题提交一次 Git
- 最后重构:做三档任务(抽业务层、换链表实现),体会"接口不变、实现全换"
三个纯文本文件,逗号分隔,可以直接用记事本打开看:
data/user.data 用户ID,用户名,密码,手机号,邮箱,姓名
data/book.data 图书ID,书名,作者,出版社,库存
data/borrow.data 用户ID,图书ID,借出日期,归还日期,状态(0=借出未还,1=已归还)
程序启动时把文件读进内存,之后每次增删改都会立刻写回文件。 不要在程序运行时手工编辑这些文件,否则内存和文件会不一致; 想手动改,先退出程序,改完再启动(或者干脆删掉文件,让程序从空数据开始)。
开发过程中踩过一个很典型的坑,写在这里当教材:
用
./library > 输出.txt的方式运行程序,输入被重定向读完(EOF)后, 某个读取函数陷入了死循环,每秒往文件里写几百 MB,十几分钟把整块硬盘写满了。
根因是:输入结束后 read_str() 一直返回空串,而"输入不能为空"的校验循环没退出。
修法是引入 input_ended()(见 src/util.c)并在每个菜单循环里检查它。
这件事的教训,比代码本身更值钱:
- 每个
while (1)都要有明确的退出条件,包括"输入没了"这种情况 - 输入中断时不要写数据——否则会把空值当用户输入存进文件
- 别用
> 文件跑交互式程序;要跑就用ulimit -f限一下文件大小,或者用timeout
- 阅读指南:分层图、三条主线调用链、推荐阅读顺序、自查题
- 练习任务:12 个任务,从改常量到抽业务层
- 如何用 Git 学习:clone / 分支 / 提交 / PR 的完整流程
MIT——随便用,拿来交作业、改成自己的项目都行。