编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

    • 入门教程

    • 综合案例

      • README
      • 学生管理通讯录系统
      • 银行账户管理系统
      • 校园身份预约系统
        • 渐进学习节奏
        • 案例元信息
        • 目录快速导航
        • 01.系统需求
          • 1.1 三种身份功能矩阵
          • 1.2 机房信息与C语言技术栈
        • 02.身份枚举与数据结构
          • 2.0 灵魂三问
          • 2.1 完整数据结构定义
        • 03.登录验证
        • 04.主菜单分发
        • 05.学生功能
        • 06.教师功能与机房容量校验
          • 6.1 教师菜单与审核
          • 6.2 机房容量校验 — 造bug → 修复
        • 07.管理员功能
        • 08.文件持久化
          • 8.1 用户文件 users.txt
          • 8.2 预约文件 orders.txt
          • 8.3 加载顺序与外键校验
        • 09.总结与C++对比
          • 9.1 本关掌握的核心能力
          • 9.2 架构对比
          • 9.3 代码对比:多态分发
        • 10.延伸挑战
      • Json与内存数据库
      • 订单票务购买系统
      • 迷你KV存储引擎器
      • 迷你编译器解释器
    • 专栏博客

    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

  • CodeX
  • C语言入门精通
  • 综合案例
杨充
2026-06-08
目录

校园身份预约系统

# 第三章:C语言 校园身份预约系统

本章是综合案例的第三关·多态入门——从"银行账户"的单一数据结构,升级到学生/教师/管理员三种身份、各自不同的菜单。核心挑战是:在C语言没有继承和多态的情况下,如何优雅地实现同一套框架支持三种不同的行为?

答案:函数指针表(手动vtable)——这是C语言实现"多态"的标准手法,也是理解C++虚函数表底层原理的最佳入口。本案例会系统展示:三身份分层 → 枚举状态机 → 多文件持久化三件事,构建一个接近真实项目规模的CLI系统。

对比前两关,本关新增四个挑战:

  1. 身份抽象:enum UserRole + 函数指针表 — C语言版的"多态分发"
  2. 枚举状态机:预约的"待审→通过/拒绝→取消"贯穿全程
  3. 多文件协同:users.txt + orders.txt 两个文件独立读写
  4. 权限分离:学生只能看自己的预约,教师能审所有预约,管理员能操作全局

⚠️ 本关新增"造bug高峰":

  • §06 预约时机房容量不检查 → OOM式超订 → 亲手修复
  • §08 文件加载顺序错 → 孤儿预约全丢 → 学会"被引用方先加载"

# 渐进学习节奏

先读这段,再开始敲代码!本案例按四阶段渐进推进:

阶段 ① 数据结构 + 登录(§02-04)· 40 min
   └ Step 1.1: enum 身份 + struct 用户/预约/机房 → 编译过
   └ Step 1.2: 登录验证 → 账号密码匹配 → 三种身份登录成功
   └ Step 1.3: 主菜单 → switch 分发到各身份子菜单

阶段 ② 各身份功能(§05-07)· 80 min  【枚举状态机 + 权限分离】
   └ Step 2.1: 学生功能 → 申请预约/查看我的/取消
   └ Step 2.2: 教师功能 → 查看全部/审核预约
   └ Step 2.3: 管理员功能 → 添加账号/查看账号/清空预约

阶段 ③ 机房容量校验(§06.2)· 15 min  【造bug → 修复】
   └ Step 3.1: 故意不检查容量 → 超订 → 亲眼看见bug
   └ Step 3.2: 添加容量检查 → 拒绝超订 → 理解防御式编程

阶段 ④ 文件持久化(§08)· 25 min  【多文件协同】
   └ Step 4.1: users.txt 保存/加载
   └ Step 4.2: orders.txt 保存/加载 → 演示加载顺序bug
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

🎯 每个 Step 必须做的三件事:

  1. 看 🎯 阶段目标卡片:明确这一阶段做什么、不做什么、验收标准
  2. 写一小段代码就编译运行一次(看到 🧪 标志立刻动手)
  3. 看到预期输出再写下一个 Step

⚠️ C语言新手最易犯的五个错:

  1. scanf 后忘记 getchar() → 下次输入被跳过
  2. 数组访问不检查 count < MAX → segfault
  3. fscanf 返回值不检查 → 静默跳过损坏行
  4. enum 当 int 用 → 类型不安全(对比 C++ enum class)
  5. 文件加载顺序错 → 孤儿数据(§08 现场演示)

🎯 本案例的"灵魂三问"(动手前先想清楚):

  • §03 登录前:三种身份怎么在同一个 users 数组里区分?是用三个数组还是带 role 字段?
  • §06 预约前:预约记录怎么跟用户关联?机房容量怎么跟预约数量关联?
  • §08 持久化前:两个文件的加载顺序重要吗?

⚠️ 本案例的四处"陷阱预警":

  • §03 登录越权:不校验 role 与登录选择的身份匹配 → 教师用学生密码也能登
  • §06 机房超订:不检查容量 → 20人机房预约了50人(§06.2 演示)
  • §07 允许对自己审核:管理员身份跳过所有校验(§07 讨论)
  • §08 加载顺序错:先加载预约再加载用户 → 外键找不到(§08 演示)

# 案例元信息

项目 说明
难度 ★★★☆☆
预估时长 3 小时(跟打 2h + 自测 1h)
前置章节 结构体、枚举、文件操作、函数指针
覆盖知识点 typedef struct、typedef enum、函数指针表、fopen/fprintf/fscanf、枚举状态机、多文件协同、外键关联
设计亮点 枚举状态机 + 权限分离 + 函数指针模拟多态 + 加载顺序与外键校验
⚠ 已知局限 密码明文存储、无加密、无并发、无数据库事务
最终产物 单一可执行文件 + 两个数据文件 users.txt/orders.txt
代码规模 约 600 行 / 1 个源文件

# 目录快速导航

  • 渐进学习节奏 【🔑 必读】
  • 案例元信息
  • 01.系统需求
    • 1.1 三种身份功能矩阵
    • 1.2 机房信息与C语言技术栈
  • 02.身份枚举与数据结构 【阶段①·灵魂三问⭐】
  • 03.登录验证
  • 04.主菜单分发 【switch多态】
  • 05.学生功能
  • 06.教师功能与机房容量校验 【造bug高峰⭐】
  • 07.管理员功能
  • 08.文件持久化 【加载顺序bug演示⭐】
  • 09.总结与C++对比
  • 10.延伸挑战

# 01.系统需求

# 1.1 三种身份功能矩阵

功能 学生 教师 管理员
登录 ✅ ✅ ✅
申请预约机房 ✅ ❌ ❌
查看自己的预约 ✅ ❌ ❌
取消自己的预约 ✅ ❌ ❌
查看所有预约 ❌ ✅ ✅
审核预约 ❌ ✅ ❌
添加学生/教师账号 ❌ ❌ ✅
查看所有账号 ❌ ❌ ✅
清空预约 ❌ ❌ ✅

这种"按身份分发功能"的设计在工业界叫 RBAC(基于角色的访问控制)。

# 1.2 机房信息与C语言技术栈

机房 容量
1号机房 20人
2号机房 50人
3号机房 100人
C语言技术 在本案例中的应用
typedef struct User、Order、Room 三大结构体
typedef enum UserRole、OrderStatus 两大枚举
数组 定长数组存储所有数据
fopen/fprintf/fscanf 文本文件读写
线性查找 根据 ID 查用户、根据订单号查预约
枚举状态机 预约:待审→通过/拒绝→取消

# 02.身份枚举与数据结构

┌─ 🎯 阶段 ① 目标 ────────────────────────────────────┐
│ 完成什么:定义三大结构体 + 两个枚举 + 全局数组 + 登录验证       │
│ 不做什么:不写任何业务功能——各个子菜单先放占位               │
│ 验收标准:编译通过 + 三种身份登录成功 + 各自看到不同菜单       │
│ 预计耗时:40 分钟                                           │
│ 关键思路:enum 区分身份 → struct 装数据 → 全局数组做容器      │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7

# 2.0 灵魂三问

🎯 在敲键盘之前,停 2 分钟问自己三个灵魂问题。

❓ 问题一:三种身份怎么在同一个 users 数组里区分?

来看反例——三个独立的数组:

// ❌ 反例:学生/教师/管理员各一个数组
UserStudent  students[100];  int studentCount = 0;
UserTeacher  teachers[100];  int teacherCount = 0;
UserAdmin    admins[10];     int adminCount = 0;
1
2
3
4

问题:

  1. 三个几乎一样的结构体——字段重复 3 次(id/name/password 完全一样)
  2. 登录时要遍历 3 个数组——查找逻辑重复 3 次
  3. 添加用户要判断 3 种类型——switch 蔓延

✅ 正确做法:一个结构体 + 一个 role 字段区分身份。

typedef enum { ROLE_STUDENT = 1, ROLE_TEACHER = 2, ROLE_ADMIN = 3 } UserRole;

typedef struct {
    int      id;
    char     name[50];
    char     password[20];
    UserRole role;   // ⭐ 一个字段区分身份
} User;

User users[100];
int   userCount = 0;
1
2
3
4
5
6
7
8
9
10
11

好处:登录时遍历一个数组 users[i],查找一个函数搞定三种身份。

❓ 问题二:预约记录怎么跟用户关联?

在 C 语言里(没有外键约束),关联靠**"存对方的 ID + 使用时再查"**:

typedef struct {
    int         orderId;
    int         studentId;      // ⭐ 外键:哪个学生提交的
    char        studentName[50]; // 冗余:避免每次查用户表
    int         roomId;          // ⭐ 外键:哪个机房
    // ...
    OrderStatus status;
} Order;
1
2
3
4
5
6
7
8

为什么 studentName 是冗余? 因为 studentId 已经能查到姓名了。但这里故意冗余——让你体验"反范式设计":用空间换时间,减少一次数组遍历。真实数据库系统里这叫"视图/物化视图"。

❓ 问题三:为什么用 enum 而不是 int 表示状态?

来看反例——用 int 表示状态:

// ❌ 反例:魔术数字
orders[i].status = 0;   // 0 是什么?待审?已通过?没人知道
orders[i].status = -1;  // -1 又是什么?

// 三个月后回来看代码:完全忘了 0/-1/1/2 分别代表什么
if (orders[i].status == 0) { ... }  // ← 这是什么状态?
1
2
3
4
5
6

✅ 正确做法:typedef enum 赋予语义。

typedef enum {
    STATUS_PENDING  = 0,   // 待审核
    STATUS_APPROVED = 1,   // 已通过
    STATUS_REJECTED = -1,  // 已拒绝
    STATUS_CANCELED = 2    // 已取消
} OrderStatus;

if (orders[i].status == STATUS_PENDING) { ... }  // ← 一目了然
1
2
3
4
5
6
7
8

🔑 三问连起来的领悟:"一个结构体带 role 字段 + 一个枚举表状态 + 一个外键关联" — 这就是 C 语言版的"关系模型"。没有继承、没有外键约束,全靠字段约定和手动校验。

# 2.1 完整数据结构定义

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_USERS    100
#define MAX_ORDERS   500
#define NAME_LEN      50

// ===== 枚举定义 =====
typedef enum {
    ROLE_STUDENT = 1,
    ROLE_TEACHER = 2,
    ROLE_ADMIN   = 3
} UserRole;

typedef enum {
    STATUS_PENDING  = 0,
    STATUS_APPROVED = 1,
    STATUS_REJECTED = -1,
    STATUS_CANCELED = 2
} OrderStatus;

// ===== 结构体定义 =====
typedef struct {
    int      id;
    char     name[NAME_LEN];
    char     password[20];
    UserRole role;
} User;

typedef struct {
    int         orderId;
    int         studentId;
    char        studentName[NAME_LEN];
    int         roomId;
    char        date[11];      // YYYY-MM-DD
    char        timeSlot[10];  // 上午/下午
    int         peopleCount;
    OrderStatus status;
} Order;

typedef struct {
    int roomId;
    int capacity;
} Room;

// ===== 全局数据 =====
User  users[MAX_USERS];
int   userCount = 0;

Order orders[MAX_ORDERS];
int   orderCount = 0;
int   orderIdCounter = 1;

Room rooms[] = { {1,20}, {2,50}, {3,100} };
int  roomCount = 3;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56

🔑 数组大小为什么这样选? MAX_USERS=100 — 学校人数有上限,100 够用。MAX_ORDERS=500 — 每个学生可能有多条预约,500 是适度估计。在 C 语言里你需要主动预估上限——C++ 的 vector 替你做这件事。

┌─ 📌 Step 1.1 小结 ────────────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • typedef enum 定义语义枚举                                │
│   • typedef struct 定义三大数据载体                          │
│   • 全局数组 + 计数器 = C语言版的"容器"                      │
│   • 枚举状态机:PENDING → APPROVED/REJECTED/CANCELED       │
│ 💡 领悟:                                                     │
│   "C语言的'关系模型' = 外键字段 + 手动校验。                  │
│    C++ 有 map, C 只有数组——但设计思想是一样的"               │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10

# 03.登录验证

// 按 ID + 密码查找用户,返回索引,找不到返回 -1
int findUser(int id, const char *password) {
    for (int i = 0; i < userCount; i++) {
        if (users[i].id == id &&
            strcmp(users[i].password, password) == 0) {
            return i;
        }
    }
    return -1;
}

// 登录主流程:返回 1 成功、0 失败
int login(UserRole *outRole, char *outName, int *outId) {
    int id;
    char password[20];
    printf("请输入账号: "); scanf("%d", &id);
    printf("请输入密码: "); scanf("%19s", password);

    int idx = findUser(id, password);
    if (idx == -1) {
        printf(">>> 账号或密码错误!\n");
        return 0;
    }
    *outRole = users[idx].role;
    strcpy(outName, users[idx].name);
    *outId   = users[idx].id;
    printf(">>> 登录成功!欢迎 %s\n", users[idx].name);
    return 1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

🔑 指针做输出参数:C 语言没有"返回多个值"的能力。login() 需要返回 3 个值(角色/姓名/ID),做法就是用 UserRole *outRole + char *outName + int *outId 三个指针参数。这在 C 语言里是最常见的模式。

🧪 测试登录:

gcc -std=c11 main.c -o campus_sys
./campus_sys
1
2

操作:必须先有账号才能登录。临时在 main 开头添加种子数据:

// 临时种子数据
users[0].id = 1001; strcpy(users[0].name, "张三");
strcpy(users[0].password, "123456"); users[0].role = ROLE_STUDENT;
users[1].id = 2001; strcpy(users[1].name, "李老师");
strcpy(users[1].password, "654321"); users[1].role = ROLE_TEACHER;
users[2].id = 0;    strcpy(users[2].name, "admin");
strcpy(users[2].password, "admin123"); users[2].role = ROLE_ADMIN;
userCount = 3;
1
2
3
4
5
6
7
8

# 04.主菜单分发

void showMainMenu() {
    printf("\n===== 校园机房预约系统 =====\n");
    printf("1. 学生登录\n");
    printf("2. 教师登录\n");
    printf("3. 管理员登录\n");
    printf("0. 退出\n");
    printf("=============================\n");
    printf("请选择: ");
}

int main() {
    loadAllData();  // 阶段 ④:启动时加载

    int choice;
    while (1) {
        showMainMenu();
        scanf("%d", &choice);
        getchar();

        if (choice == 0) {
            printf("再见!\n");
            saveAllData();  // 阶段 ④:退出前保存
            return 0;
        }
        if (choice < 1 || choice > 3) {
            printf("无效选择!\n");
            continue;
        }

        UserRole role;
        char name[NAME_LEN];
        int userId;
        if (!login(&role, name, &userId)) continue;

        // ⭐ 根据角色进入不同子菜单 — C语言的"多态分发"
        switch (role) {
            case ROLE_STUDENT: studentMenu(userId, name);   break;
            case ROLE_TEACHER: teacherMenu(userId, name);   break;
            case ROLE_ADMIN:   adminMenu(userId, name);     break;
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

🔑 设计要点:main 函数只负责"身份分发"——不侵入任何角色的业务逻辑。将来加"访客"身份只需加一个 case 和一个新函数,不动现有代码。这就是命令模式的雏形,在 C 语言里用 switch + enum 实现。

┌─ 📌 阶段 ① 小结 ────────────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • 一个 enum + 一个 struct 装三种身份                 		│
│   • login 通过指针输出多个返回值                               │
│   • main 的 switch(role) 是 C 语言的"多态分发"                 │
│ ⏸ 还没碰的(下阶段才会做):                                   │
│   • 学生/教师/管理员的真实功能                                  │
│   • 文件持久化                                                  │
│ 💡 本阶段最大领悟:                                            │
│   "C语言的继承 = enum role 字段 + switch 分发"                 │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11

# 05.学生功能

┌─ 🎯 阶段 ② 目标(学生) ─────────────────────────────┐
│ 完成什么:学生登录 → 申请预约/查看我的预约/取消预约          │
│ 不做什么:不做容量校验——那是 §06 的造bug环节                  │
│ 验收标准:申请 → 看到 → 取消 → 再到查看(状态变为已取消)     │
│ 预计耗时:30 分钟                                            │
└────────────────────────────────────────────────────────┘
1
2
3
4
5
6
void studentMenu(int userId, const char *name) {
    int choice;
    while (1) {
        printf("\n--- 学生菜单 (%s) ---\n", name);
        printf("1. 申请预约  2. 查看我的预约\n");
        printf("3. 取消预约  0. 注销登录\n");
        printf("请选择: ");
        scanf("%d", &choice);
        getchar();
        switch (choice) {
            case 1: studentApply(userId, name);  break;
            case 2: studentView(userId);         break;
            case 3: studentCancel(userId);       break;
            case 0: return;
        }
    }
}

void studentApply(int userId, const char *name) {
    if (orderCount >= MAX_ORDERS) {
        printf("预约已满!\n"); return;
    }

    // 显示可用机房
    printf("\n--- 可用机房 ---\n");
    for (int i = 0; i < roomCount; i++) {
        printf("机房%d: 容量%d人\n", rooms[i].roomId, rooms[i].capacity);
    }

    Order *o = &orders[orderCount];
    o->orderId = orderIdCounter++;
    o->studentId = userId;
    strcpy(o->studentName, name);

    printf("选择机房 (1/2/3): "); scanf("%d", &o->roomId);
    printf("日期 (YYYY-MM-DD): ");  scanf("%10s", o->date);
    printf("时段 (上午/下午): ");    scanf("%9s", o->timeSlot);
    printf("人数: ");              scanf("%d", &o->peopleCount);

    // ⚠️ 注意:这里暂时不检查机房容量——§06 会故意让这个 bug 暴露
    o->status = STATUS_PENDING;
    orderCount++;
    printf(">>> 预约提交成功!订单号: %d(待教师审核)\n", o->orderId);
}

void studentView(int userId) {
    int found = 0;
    printf("\n--- 我的预约 ---\n");
    printf("订单号 | 日期       | 机房 | 时段 | 人数 | 状态\n");
    printf("--------------------------------------------------\n");
    for (int i = 0; i < orderCount; i++) {
        if (orders[i].studentId == userId) {
            const char *statusText;
            switch (orders[i].status) {
                case STATUS_PENDING:  statusText = "待审核"; break;
                case STATUS_APPROVED: statusText = "已通过"; break;
                case STATUS_REJECTED: statusText = "已拒绝"; break;
                case STATUS_CANCELED: statusText = "已取消"; break;
                default:              statusText = "未知";
            }
            printf("%-7d | %-10s | %-4d | %-4s | %-4d | %s\n",
                   orders[i].orderId, orders[i].date,
                   orders[i].roomId, orders[i].timeSlot,
                   orders[i].peopleCount, statusText);
            found = 1;
        }
    }
    if (!found) printf("暂无预约记录。\n");
}

void studentCancel(int userId) {
    int orderId;
    printf("请输入要取消的订单号: "); scanf("%d", &orderId);
    for (int i = 0; i < orderCount; i++) {
        if (orders[i].orderId == orderId &&
            orders[i].studentId == userId) {
            if (orders[i].status == STATUS_CANCELED) {
                printf("该预约已取消!\n"); return;
            }
            if (orders[i].status != STATUS_PENDING) {
                printf("该预约已被审核,无法取消!\n"); return;
            }
            orders[i].status = STATUS_CANCELED;
            printf(">>> 取消成功!\n"); return;
        }
    }
    printf("未找到该订单或无权限!\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88

🔑 三句关键的权限校验:

  • orders[i].studentId == userId — 学生只能操作自己的预约
  • orders[i].status != STATUS_PENDING — 已审核的预约不能取消
  • orders[i].status == STATUS_CANCELED — 已取消的不能重复取消

这就是权限分离在C语言里的体现:没有 private 字段,全靠 if-else 手动校验。

🧪 测试学生功能:

# 操作:登录学生(1001) → 选 1 → 机房1 2026-06-01 上午 15人
>>> 预约提交成功!订单号: 1(待教师审核)

# 选 2 查看
订单号 | 日期       | 机房 | 时段 | 人数 | 状态
--------------------------------------------------
1      | 2026-06-01 |    1 | 上午 |   15 | 待审核

# 选 3 → 订单号 1
>>> 取消成功!
1
2
3
4
5
6
7
8
9
10

# 06.教师功能与机房容量校验

┌─ 🎯 阶段 ② 目标(教师) ─────────────────────────────┐
│ 完成什么:教师登录 → 查看所有预约 → 审核通过/拒绝          │
│ 关键环节:§6.2 故意不检查容量 → 超订 → 亲手修复           │
│ 验收标准:审核后状态变更 + 机房容量超限被拒绝              │
│ 预计耗时:25 分钟                                         │
└────────────────────────────────────────────────────────┘
1
2
3
4
5
6

# 6.1 教师菜单与审核

void teacherMenu(int userId, const char *name) {
    int choice;
    while (1) {
        printf("\n--- 教师菜单 (%s) ---\n", name);
        printf("1. 查看所有预约  2. 审核预约  0. 注销\n");
        printf("请选择: ");
        scanf("%d", &choice);
        getchar();
        switch (choice) {
            case 1: teacherViewAll();  break;
            case 2: teacherReview();   break;
            case 0: return;
        }
    }
}

void teacherViewAll() {
    printf("\n--- 所有预约 ---\n");
    if (orderCount == 0) { printf("暂无预约记录。\n"); return; }
    printf("订单号 | 学生   | 日期       | 机房 | 时段 | 人数 | 状态\n");
    printf("------------------------------------------------------------\n");
    for (int i = 0; i < orderCount; i++) {
        const char *statusText;
        switch (orders[i].status) {
            case STATUS_PENDING:  statusText = "待审核"; break;
            case STATUS_APPROVED: statusText = "已通过"; break;
            case STATUS_REJECTED: statusText = "已拒绝"; break;
            case STATUS_CANCELED: statusText = "已取消"; break;
            default:              statusText = "未知";
        }
        printf("%-7d | %-5s | %-10s | %-4d | %-4s | %-4d | %s\n",
               orders[i].orderId, orders[i].studentName, orders[i].date,
               orders[i].roomId, orders[i].timeSlot,
               orders[i].peopleCount, statusText);
    }
}

void teacherReview() {
    int orderId, decision;
    printf("请输入订单号: "); scanf("%d", &orderId);

    for (int i = 0; i < orderCount; i++) {
        if (orders[i].orderId == orderId) {
            if (orders[i].status != STATUS_PENDING) {
                printf("该预约已被处理!当前状态不可审核。\n"); return;
            }
            printf("\n当前预约: %s | 机房%d | %s | %d人\n",
                   orders[i].studentName, orders[i].roomId,
                   orders[i].timeSlot, orders[i].peopleCount);
            printf("1.通过  2.拒绝: "); scanf("%d", &decision);
            orders[i].status = (decision == 1) ?
                STATUS_APPROVED : STATUS_REJECTED;
            printf(">>> 审核完成!\n");
            return;
        }
    }
    printf("未找到订单!\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58

# 6.2 机房容量校验 — 造bug → 修复

┌─ ⚠️ 造BUG高峰 ────────────────────────────────────┐
│ 先让学生预约机房人数超过容量 → 让你看到"超订"          │
│ 然后在审核环节加容量检查 → 理解"防御式编程"            │
└──────────────────────────────────────────────────────┘
1
2
3
4

❓ 灵魂一问:学生申请预约时,机房 1 号容量是 20 人,如果 10 个学生各申请 15 人——会发生什么?

回答前先跑一次:

# 操作:连续用 3 个学生账号,每个都预约机房1、15人
学生A: 机房1 15人 → 订单1 Pending
学生B: 机房1 15人 → 订单2 Pending   ← 两个预约加起来 30 人,机房容量才 20!
学生C: 机房1 15人 → 订单3 Pending   ← 45 人!严重超订
1
2
3
4

现在看 studentApply 的代码——完全没有检查 o->peopleCount <= rooms[].capacity!这就是 bug。

🛠 修复方案:在教师审核时加上容量检查。为什么不在学生申请时检查? 因为学生申请时可能一个机房有多个 Pending 预约——最终是否超限要等审核结果。更简单的做法:审核通过时统计该机房所有已通过的预约人数,加上当前这个,判断是否超限。

void teacherReview() {
    // ... (前面查找逻辑不变) ...

    // ⭐ 容量检查:统计该机房已通过的预约总人数
    if (decision == 1) {  // 只有"通过"才需要检查
        int totalPeople = orders[i].peopleCount;
        for (int j = 0; j < orderCount; j++) {
            if (orders[j].roomId == orders[i].roomId &&
                orders[j].status == STATUS_APPROVED &&
                orders[j].orderId != orders[i].orderId) {
                totalPeople += orders[j].peopleCount;
            }
        }
        // 找到对应机房的容量
        int capacity = 0;
        for (int r = 0; r < roomCount; r++) {
            if (rooms[r].roomId == orders[i].roomId) {
                capacity = rooms[r].capacity;
                break;
            }
        }
        if (totalPeople > capacity) {
            printf(">>> 审核拒绝:机房%d 容量%d人,已审批+当前申请=%d人,超限!\n",
                   orders[i].roomId, capacity, totalPeople);
            return;  // 拒绝通过,维持 Pending 状态让教师手动拒绝
        }
    }

    orders[i].status = (decision == 1) ? STATUS_APPROVED : STATUS_REJECTED;
    printf(">>> 审核完成!\n");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

🧪 修复后测试:

# 场景:机房1 容量20人
# 学生A 申请15人 → 教师审核通过 → 通过(15 &lt;= 20)✅
# 学生B 申请15人 → 教师审核 → 
>>> 审核拒绝:机房1 容量20人,已审批+当前申请=30人,超限!
1
2
3
4

🔑 核心领悟:校验要放在"写操作"之前——这和银行案例的转账前置校验是同一原则。C 语言没有异常机制,全靠 if-else 手动做"前置条件检查"。

┌─ 📌 阶段 ② 小结 ────────────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • 学生:申请/查看/取消 — 三次权限校验                         │
│   • 教师:查看全部/审核 — 权限不受限(可看所有预约)            │
│   • 容量校验:亲手看见超订 bug → 修复为前置容量检查             │
│                                                             │
│ 🔑 亲眼看见的关键现象:                                        │
│   • 不加容量检查 → 20人机房预约 45人 → "超订"                  │
│   • 修复后 → 审核通过前统计已有审批人数 → 超限拒绝             │
│                                                             │
│ 💡 本阶段最大领悟:                                            │
│   "C语言的权限 = if (studentId == userId),                  │
│    C语言的容量校验 = 遍历已有数据 + if (total > capacity)。    │
│    没有框架替你验证,全靠你自己写检查"                         │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 07.管理员功能

void adminMenu(int userId, const char *name) {
    int choice;
    while (1) {
        printf("\n--- 管理员菜单 (%s) ---\n", name);
        printf("1. 添加账号  2. 查看所有账号\n");
        printf("3. 清空预约  0. 注销\n");
        printf("请选择: ");
        scanf("%d", &choice);
        getchar();
        switch (choice) {
            case 1: adminAddUser();     break;
            case 2: adminViewAll();     break;
            case 3: adminClearOrders(); break;
            case 0: return;
        }
    }
}

void adminAddUser() {
    if (userCount >= MAX_USERS) {
        printf("用户数已达上限!\n"); return;
    }

    // ⚠️ 先检查 ID 是否已存在
    int newId, roleChoice;
    printf("身份 (1.学生 2.教师): "); scanf("%d", &roleChoice);
    if (roleChoice != 1 && roleChoice != 2) {
        printf("无效身份!只能添加学生或教师。\n"); return;
    }
    printf("账号ID: "); scanf("%d", &newId);

    // ⭐ ID 唯一性检查
    for (int i = 0; i < userCount; i++) {
        if (users[i].id == newId) {
            printf("该ID已被占用!\n"); return;
        }
    }

    User *u = &users[userCount];
    u->id = newId;
    u->role = (roleChoice == 1) ? ROLE_STUDENT : ROLE_TEACHER;
    printf("姓名: "); scanf("%49s", u->name);
    printf("密码: "); scanf("%19s", u->password);
    userCount++;
    printf(">>> 账号添加成功!当前用户总数: %d\n", userCount);
}

void adminViewAll() {
    printf("\n--- 所有账号 ---\n");
    printf("ID   | 姓名     | 身份\n");
    printf("----------------------\n");
    for (int i = 0; i < userCount; i++) {
        const char *roleText;
        switch (users[i].role) {
            case ROLE_STUDENT: roleText = "学生"; break;
            case ROLE_TEACHER: roleText = "教师"; break;
            case ROLE_ADMIN:   roleText = "管理员"; break;
            default:           roleText = "未知";
        }
        printf("%-4d | %-8s | %s\n",
               users[i].id, users[i].name, roleText);
    }
    printf("----------------------\n");
    printf("共 %d 个用户\n", userCount);
}

void adminClearOrders() {
    char confirm;
    printf("确认清空所有预约记录?(y/n): ");
    scanf(" %c", &confirm);
    if (confirm == 'y' || confirm == 'Y') {
        orderCount = 0;
        orderIdCounter = 1;
        printf(">>> 已清空所有预约记录!\n");
    } else {
        printf("操作已取消。\n");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78

🔑 管理员权限的最高级别:

  • adminAddUser — 可以创建新用户(不限制是否管理员本人)
  • adminClearOrders — 可以清空所有预约(需要二次确认 y/n)
  • 管理员不能自己审核预约—那是教师的职责

⚠️ 安全提醒:管理员不能通过菜单创建另一个管理员(roleChoice 只允许 1 或 2)。这是简易的"权限隔离"——真实 RBAC 系统会用更复杂的层级管理。


# 08.文件持久化

┌─ 🎯 阶段 ③ 目标 ────────────────────────────────────┐
│ 完成什么:程序退出后数据不丢 — 2 个文本文件协同读写         │
│ 不做什么:不做加密、不做备份、不做 CSV 格式                │
│ 验收标准:加账号+预约 → 退出 → 重启 → 数据完整恢复       │
│ 预计耗时:25 分钟                                          │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6

🔑 为什么用文本格式而不是二进制?

格式 可读性 可手动编辑 跨平台 本案例选择
文本(fprintf) ✅ cat 可看 ✅ 可 vim 改 ✅ 通用 本案例
二进制(fwrite) ❌ 乱码 ❌ 需 hex 编辑器 ⚠️ 端序 第 01 关

# 8.1 用户文件 users.txt

格式: ID 姓名 密码 角色(1=学生 2=教师 3=管理员)
1001 张三 123456 1
2001 李老师 654321 2
0 admin admin123 3
1
2
3
4
#define USERS_FILE  "users.txt"
#define ORDERS_FILE "orders.txt"

void saveUsers() {
    FILE *f = fopen(USERS_FILE, "w");
    if (!f) { printf("无法打开 %s!\n", USERS_FILE); return; }
    for (int i = 0; i < userCount; i++) {
        fprintf(f, "%d %s %s %d\n",
                users[i].id, users[i].name,
                users[i].password, (int)users[i].role);
    }
    fclose(f);
    printf(">>> 已保存 %d 个用户到 %s\n", userCount, USERS_FILE);
}

void loadUsers() {
    FILE *f = fopen(USERS_FILE, "r");
    if (!f) {
        printf("未找到 %s,将使用默认管理员账号。\n", USERS_FILE);
        // 首次启动:自动创建默认管理员
        users[0].id = 0;
        strcpy(users[0].name, "admin");
        strcpy(users[0].password, "admin123");
        users[0].role = ROLE_ADMIN;
        userCount = 1;
        return;
    }
    userCount = 0;
    int roleInt;
    while (userCount < MAX_USERS &&
           fscanf(f, "%d %s %s %d",
                  &users[userCount].id,
                  users[userCount].name,
                  users[userCount].password,
                  &roleInt) == 4) {
        users[userCount].role = (UserRole)roleInt;
        userCount++;
    }
    fclose(f);
    printf(">>> 已加载 %d 个用户\n", userCount);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 8.2 预约文件 orders.txt

格式: 订单ID 学生ID 学生姓名 机房ID 日期 时段 人数 状态
1 1001 张三 1 2026-06-01 上午 15 0
2 1002 李四 2 2026-06-02 下午 30 1
1
2
3
void saveOrders() {
    FILE *f = fopen(ORDERS_FILE, "w");
    if (!f) { printf("无法打开 %s!\n", ORDERS_FILE); return; }
    for (int i = 0; i < orderCount; i++) {
        fprintf(f, "%d %d %s %d %s %s %d %d\n",
                orders[i].orderId, orders[i].studentId,
                orders[i].studentName, orders[i].roomId,
                orders[i].date, orders[i].timeSlot,
                orders[i].peopleCount, (int)orders[i].status);
    }
    fclose(f);
    printf(">>> 已保存 %d 条预约到 %s\n", orderCount, ORDERS_FILE);
}

void loadOrders() {
    FILE *f = fopen(ORDERS_FILE, "r");
    if (!f) { orderCount = 0; return; }
    orderCount = 0;
    int statusInt, maxOrderId = 0;
    while (orderCount < MAX_ORDERS &&
           fscanf(f, "%d %d %s %d %s %s %d %d",
                  &orders[orderCount].orderId,
                  &orders[orderCount].studentId,
                  orders[orderCount].studentName,
                  &orders[orderCount].roomId,
                  orders[orderCount].date,
                  orders[orderCount].timeSlot,
                  &orders[orderCount].peopleCount,
                  &statusInt) == 8) {
        orders[orderCount].status = (OrderStatus)statusInt;
        if (orders[orderCount].orderId > maxOrderId) {
            maxOrderId = orders[orderCount].orderId;
        }
        orderCount++;
    }
    fclose(f);
    orderIdCounter = maxOrderId + 1;  // 恢复自增计数器
    printf(">>> 已加载 %d 条预约\n", orderCount);
}

// 统一入口
void loadAllData() {
    loadUsers();    // ⭐ 先加载被引用方
    loadOrders();   // ⭐ 再加载引用方(预约引用了用户ID)
}

void saveAllData() {
    saveUsers();
    saveOrders();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50

# 8.3 加载顺序与外键校验

⚠️ 经典错误:先加载预约再加载用户——预定记录的 studentId 引用了用户表的主键,如果用户表还没加载,外键校验找不到人。

// ❌ 错误版本
void loadAllData_WRONG() {
    loadOrders();   // ← 先加载预约:此时 userCount = 0,所有预约的 studentId 都是"孤儿"
    loadUsers();    // ← 后加载用户:太晚了,预约已经加载完了
}
1
2
3
4
5

本案例中预约文件加载时不做活跃的外键校验(因为 loadOrders 设计为只负责读数据),但顺序决定了数据一致性。在真实系统中,应该在加载预约后扫描一遍,标记出 studentId 在用户表中不存在的"孤儿预约"。

🧪 测试持久化(完整二轮测试):

# 第一轮:创建数据
$ ./campus_sys
未找到 users.txt,将使用默认管理员账号。   ← 首次启动
>>> 已加载 1 个用户

请选择: 3  (管理员登录)
账号: 0  密码: admin123
>>> 登录成功!欢迎 admin

--- 管理员菜单 ---
1. 添加账号  ...
选 1 → 学生 1001 张三 123 → 添加成功
选 1 → 学生 1002 李四 456 → 添加成功

# 退出 → 登学生1001 → 预约机房1 → 退出 → 登教师2001 → 审核

选 0 → 再见!(触发 saveAllData)

$ cat users.txt
0 admin admin123 3
1001 张三 123 1
1002 李四 456 1

$ cat orders.txt
1 1001 张三 1 2026-06-01 上午 15 1      ← status=1 表示已通过

# 第二轮:冷启动验证
$ ./campus_sys
>>> 已加载 3 个用户
>>> 已加载 1 条预约

选 1 → 账号 1001 密码 123 → 登录成功 → 选 2 → 看到预约 ✅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
┌─ 📌 阶段 ③ 小结 ────────────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • users.txt + orders.txt 双文件协同读写                     │
│   • fprintf/fscanf 格式化文本持久化                           │
│   • loadAllData/saveAllData 统一入口                          │
│   • 加载顺序:被引用方(users)先于引用方(orders)               │
│   • orderIdCounter 从文件中恢复最大ID+1                      │
│                                                             │
│ 💡 本阶段最大领悟:                                            │
│   "两个文本文件 = 两张数据库表。加载顺序就是外键约束的        │
│    手动实现——被引用方先加载,引用方后加载"                    │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12

# 09.总结与C++对比

# 9.1 本关掌握的核心能力

能力 C 语言体现 掌握标志
身份多态 enum UserRole + switch 分发 能解释为什么不用三个独立数组
枚举状态机 OrderStatus — PENDING→APPROVED/REJECTED/CANCELED 能画状态转移图
外键关联 orders[i].studentId + 手动查找 能解释加载顺序为什么重要
权限分离 if (studentId == userId) 手动校验 能识别三种身份的所有权限边界
容量校验 审核时统计已有审批人数 + 对比 capacity 能解释为什么不在申请时检查
多文件持久化 两个文本文件 + 加载顺序 能 cat 文件后手工修复一条记录

# 9.2 架构对比

维度 C语言实现(本案例) C++实现(同章节C++篇)
身份抽象 enum UserRole + switch 分发 User 抽象基类 + 三态派生
多态 switch-case 手动分发 虚函数表 vtable 自动分发
容器 定长数组 users[100] + userCount map<string, shared_ptr<User>>
查找 线性遍历 O(n) map O(log n)
状态机 typedef enum enum class ResStatus 类型安全
外键 手动查找 + 加载顺序约定 map.count() + 加载后校验
文件 2 个文本文件 3 个 CSV 文件 + 类型标签
代码量 ~600 行(单文件) ~1200 行(14 个文件)

# 9.3 代码对比:多态分发

C 语言版(本案例):

// 用 switch + enum 手动分发
switch (role) {
    case ROLE_STUDENT: studentMenu(userId, name); break;
    case ROLE_TEACHER: teacherMenu(userId, name); break;
    case ROLE_ADMIN:   adminMenu(userId, name);   break;
}
// ⚠️ 加一种身份:加 case + 新函数 — 至少改 1 处
1
2
3
4
5
6
7

C++ 等效版:

// 用虚函数自动分发
shared_ptr<User> u = sys->login(id, pwd);
u->mainMenu();  // ⭐ 一行通吃三种身份 — vtable 自动分发
// ✅ 加一种身份:只新增子类 — main 一行不改
1
2
3
4

C++ 的多态就是让 switch-case 消失进 vtable 里。


# 10.延伸挑战

完成主流程后,尝试把下列四项自己加上:

挑战 1(基础)· 加身份证校验

在 login() 函数中增加校验:用户选择的登录类型(1=学生 2=教师 3=管理员)必须与 users[idx].role 一致。防止教师用学生账号登录。

// 在 login() 中追加
int selectedRole;  // 从主菜单传入
if ((UserRole)selectedRole != users[idx].role) {
    printf("身份不匹配!请选择正确的登录类型。\n");
    return 0;
}
1
2
3
4
5
6

挑战 2(进阶)· 加日期校验

在学生申请预约时,检查日期不能是过去的日期:

#include <time.h>
// 解析输入的日期字符串,转换为时间戳
// 与当前时间比较:if (inputDate < today) → 拒绝
1
2
3

挑战 3(进阶)· 用函数指针表替换 switch

体验 C 语言的真正"多态"——用函数指针模拟虚函数表:

// 定义菜单项结构体
typedef struct {
    const char *label;
    void (*action)(int userId, const char *name);
} MenuItem;

// 学生菜单的函数指针表
MenuItem studentActions[] = {
    {"申请预约",  studentApply},
    {"查看预约",  studentView},
    {"取消预约",  studentCancel},
};

// 菜单循环改造为遍历函数指针表
for (int i = 0; i < 3; i++) {
    printf("%d. %s\n", i + 1, studentActions[i].label);
}
// ... 根据输入调用 studentActions[choice-1].action(userId, name);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

完成后再对比 C++ 的虚函数表——你会发现原理一模一样。

挑战 4(终极)· 移植到 C++

把 C 版本逐模块改写成 C++,对比代码行数变化:

模块 C 版本 C++ 版本
身份定义 enum UserRole ~10 行 class User ~60 行
多态分发 switch(role) 手动 u->mainMenu() 自动
容器 User users[100] map<string, shared_ptr<User>>
查找 for + if O(n) map.find() O(log n)
文件 fprintf/fscanf ofstream/ifstream

  • ⬅ 上一案例:02.银行账户管理系统
  • ➡ 下一案例:04.JSON与内存数据库
上次更新: 2026/06/10, 11:13:41
银行账户管理系统
Json与内存数据库

← 银行账户管理系统 Json与内存数据库→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式