编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • 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 功能要求
          • 1.3 涉及知识点
        • 02.菜单骨架
          • 2.1 最小可编译版
          • 2.2 填充菜单
          • 2.3 switch 分发
        • 03.退出功能
        • 04.添加学生
          • 4.0 灵魂三问
          • 4.1 定义学生结构体
          • 4.2 定义链表节点
          • 4.3 链表尾插实现
          • 4.4 录入学生信息
          • 4.5 测试添加
        • 05.显示学生
        • 06.删除学生
          • 6.0 灵魂两问
          • 6.1 三种删除场景
          • 6.2 实现删除(修复版)
          • 6.3 Valgrind 验证无泄漏
        • 07.查找与统计
        • 08.修改学生
        • 09.文件持久化
          • 9.1 二进制保存
          • 9.2 启动时加载
          • 9.3 为什么不用 fwrite(sizeof(Node))
        • 10.项目总结
          • 10.1 整体架构
          • 10.2 核心知识点覆盖
        • 11.技术思考:C vs C++
          • 代码对比:同一功能两种实现
          • 延伸挑战
      • 银行账户管理系统
      • 校园身份预约系统
      • Json与内存数据库
      • 订单票务购买系统
      • 迷你KV存储引擎器
      • 迷你编译器解释器
    • 专栏博客

    • 标准集库

  • Cpp入门到精通

  • Java入门精通

  • Go入门到精通

  • JavaScript入门

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

学生管理通讯录系统

# 第一章:C语言 学生管理通讯录系统

本章是C语言综合案例的第一关——用结构体、指针、链表、文件操作完成一个菜单驱动的学生通讯录。代码刻意全部使用C语言标准库(printf/scanf/malloc/free/fopen),为的是在学完C++版同一案例后,你能直观感受到封装、RAII、STL容器替代了多少C语言的手动管理。

学习方式:本案例按"功能拆分 → 写代码 → 编译运行 → 看输出 → 避陷阱"五步法循环。每个功能 30 分钟内可完成,严格遵循 C 语言的工程节奏——每加 30 行就编译一次,而不是一口气抄 300 行。

⚠️ C语言独有挑战:本案例使用链表管理学生(C 没有 vector),使用文件二进制读写做持久化(C 没有 fstream),使用手动 malloc/free 管理内存(C 没有 RAII)。忘记 free 的后果会通过 Valgrind 现场演示给你看。


# 渐进学习节奏

先读这段,再开始敲代码!本案例分三个阶段推进:

阶段 ① REPL 骨架(§02-03)· 30 min
   └ Step 1.1: 写 showMenu 空函数 → 编译 → 看到菜单 ✅
   └ Step 1.2: 加 switch 分发 → 编译 → 选 0 能退出 ✅

阶段 ② 链表增删改查(§04-08)· 90 min  【链表是C语言第一道坎】
   └ Step 2.1: 设计 Student 结构体 + Node 链表节点
   └ Step 2.2: 添加学生 → 编译 → 用链表尾插 → 遍历打印验证
   └ Step 2.3: 显示全部 → 编译 → 格式化输出
   └ Step 2.4: 删除学生 → ⚠️ 先看 Valgrind 内存泄漏 → 再加 free 修复
   └ Step 2.5: 查找/修改/统计(依次类推)

阶段 ③ 文件持久化(§09)· 30 min  【C语言文件操作实战】
   └ Step 3.1: 保存到文件(fwrite 二进制)→ 退出再进,数据还在
   └ Step 3.2: 从文件读取(fread)→ 启动时自动加载
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

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

⚠️ C语言新手最容易犯的错:忘记 free → 内存泄漏 → 程序跑久了 OOM。本案例会两次用 Valgrind 演示给你看。

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

  • §04 添加学生前:为什么用链表不用数组?malloc 分配的内存放在哪里?忘记 free 会怎样?
  • §06 删除学生前:删除链表节点时,头节点/中间节点/尾节点的处理一样吗?为什么需要 beforeNode?

⚠️ 本案例的四处"陷阱预警"(C语言经典坑):

  • §04 忘记初始化 pNext = NULL → 野指针 → 遍历链表时 segfault
  • §04 scanf 缓冲区问题:scanf("%d") 后残留的 \n 会污染下一次 getchar()
  • §06 只删节点不 free → Valgrind 报 "definitely lost"
  • §09 fwrite sizeof(Node) → 写入了指针字段 → 读回来是悬垂指针

# 案例元信息

项目 说明
难度 ★★☆☆☆(C语言第一站)
预估时长 3 小时(跟打 1.5h + 自测 1.5h)
前置章节 C语言入门精通:结构体、指针、函数、文件操作
覆盖知识点 struct、typedef、malloc/free、链表尾插/删除、指针传参、switch/while、fopen/fwrite/fread/fclose、scanf/printf
设计亮点 链表替代数组——为什么C语言项目必须掌握链表
⚠ 已知局限 无并发安全(单线程程序)、无数据加密、文件无校验
最终产物 单一可执行文件 + 数据文件 stuinfo.dat
代码规模 约 400 行 / 3 个文件(main.c、StudentManager.h、StudentManager.c)

# 目录快速导航

点击以下条目即可跳转到对应节。【🔑 重点节】推荐优先阅读。

  • 渐进学习节奏 【🔑 必读】
  • 案例元信息
  • 01.系统需求
    • 1.1 需求介绍
    • 1.2 功能要求
    • 1.3 涉及知识点
  • 02.菜单骨架 【阶段①】
    • 2.1 最小可编译版
    • 2.2 填充菜单
    • 2.3 switch 分发
  • 03.退出功能
  • 04.添加学生 【阶段②·链表入门⭐】
    • 4.0 灵魂三问
    • 4.1 定义学生结构体
    • 4.2 定义链表节点
    • 4.3 链表尾插实现
    • 4.4 录入学生信息
    • 4.5 测试添加
  • 05.显示学生
  • 06.删除学生 【内存泄漏演示⭐】
    • 6.0 灵魂两问
    • 6.1 三种删除场景
    • 6.2 实现删除
    • 6.3 Valgrind 验证无泄漏
  • 07.查找与统计
  • 08.修改学生
  • 09.文件持久化 【阶段③】
    • 9.1 二进制保存
    • 9.2 启动时加载
    • 9.3 为什么不用 fwrite(sizeof(Node))
  • 10.项目总结
  • 11.技术思考:C vs C++

# 01.系统需求

# 1.1 需求介绍

用 C 语言实现一个学生信息管理系统,通过控制台菜单操作,支持录入、打印、保存、读取、查找、修改、删除学生信息。学生信息包括:学号、姓名、性别、年龄、成绩。

# 1.2 功能要求

1. 录入学生信息 —— 将学生信息添加到链表中
2. 打印学生信息 —— 遍历链表,格式化输出
3. 保存学生信息 —— 将链表数据写入文件(二进制)
4. 读取学生信息 —— 从文件加载数据到链表
5. 统计学生人数 —— 遍历链表计数
6. 查找学生信息 —— 按学号查找
7. 修改学生信息 —— 按学号找到后修改
8. 删除学生信息 —— 按学号删除节点并 free
0. 退出系统
1
2
3
4
5
6
7
8
9

# 1.3 涉及知识点

知识点 在本案例中的位置 C语言特性
结构体 §04 学生/链表节点定义 typedef struct
动态内存 §04 链表创建、§06 删除 free malloc/free
指针 §04-06 链表操作 二级指针修改头节点
文件IO §09 持久化 fopen/fwrite/fread/fclose
格式化IO §02 菜单、§05 打印 printf/scanf
流程控制 §02 switch 分发 while/switch-case
函数封装 全篇 每个功能独立函数

# 02.菜单骨架

┌─ 🎯 阶段 ① 目标 ────────────────────────────────────┐
│ 完成什么:跑通"显示菜单 → 读取输入 → 退出"               │
│ 不做什么:不接任何业务功能——所有 case 都是空壳            │
│ 验收标准:能看到 9 行菜单 + 选 0 退出 + 选其他回显提示      │
│ 预计耗时:30 分钟                                       │
│ 关键思路:先打通"输入循环"管道——所有 CLI 程序都是这个壳子  │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7

# 2.1 最小可编译版

新建 main.c,先写一个能编译的最小版本:

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

// 菜单展示
void showMenu() {
    printf("***************************************\n");
    printf("*   欢迎使用学生管理系统 v1.0         *\n");
    printf("***************************************\n");
    printf("*   1.录入学生信息                    *\n");
    printf("*   2.打印学生信息                    *\n");
    printf("*   3.保存学生信息                    *\n");
    printf("*   4.读取学生信息                    *\n");
    printf("*   5.统计学生人数                    *\n");
    printf("*   6.查找学生信息                    *\n");
    printf("*   7.修改学生信息                    *\n");
    printf("*   8.删除学生信息                    *\n");
    printf("*   0.退出系统                        *\n");
    printf("***************************************\n");
    printf("请选择: ");
}

int main() {
    showMenu();
    return 0;
}
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

🧪 立刻编译运行:

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

预期输出:9 行菜单 + "请选择: "。看到菜单说明你的第一个 C 程序已经能跑了——后面所有功能都是在这个壳里加 case。

# 2.2 填充菜单

void pauseProgram() {
    printf("按回车键继续...\n");
    while (getchar() != '\n');  // 消费缓冲区残留
    getchar();                   // 等待回车
}

void clearScreen() {
    printf("\033[2J\033[H");    // ANSI 清屏(Linux/macOS)
    // Windows 上用 system("cls");
}
1
2
3
4
5
6
7
8
9
10

# 2.3 switch 分发

int main() {
    while (1) {
        showMenu();
        int choice;
        scanf("%d", &choice);
        // ⚠️ 消费掉 scanf 残留的换行符,否则下次 getchar() 会读到它
        getchar();

        switch (choice) {
            case 1: printf(">>> 录入学生信息\n"); break;
            case 2: printf(">>> 打印学生信息\n"); break;
            case 3: printf(">>> 保存学生信息\n"); break;
            case 4: printf(">>> 读取学生信息\n"); break;
            case 5: printf(">>> 统计学生人数\n"); break;
            case 6: printf(">>> 查找学生信息\n"); break;
            case 7: printf(">>> 修改学生信息\n"); break;
            case 8: printf(">>> 删除学生信息\n"); break;
            case 0:
                printf("退出系统,再见!\n");
                return 0;
            default:
                printf("无效选择,请重新输入!\n");
                pauseProgram();
                clearScreen();
                break;
        }
    }
    return 0;
}
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
┌─ 📌 阶段 ① 小结 ────────────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • CLI 程序的主循环骨架(while + switch + 退出)              │
│   • scanf 缓冲区问题的第一次实战处理                           │
│   • 用 pauseProgram + clearScreen 做控制台 UX                │
│ ⏸ 还没碰的(下阶段才会做):                                   │
│   • 链表、malloc/free、文件读写                               │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8

# 03.退出功能

case 0:
    printf("退出系统,再见!\n");
    return 0;
1
2
3

✅ 选 0 能干净退出,不报错。阶段 ① 完成。


# 04.添加学生

┌─ 🎯 阶段 ② 目标 ────────────────────────────────────┐
│ 完成什么:定义 Student + Node 结构体,链表尾插录入信息       │
│ 不做什么:不做文件读写、不做并发                             │
│ 验收标准:连续添加 3 个学生 → 打印看到 3 条记录               │
│ 预计耗时:40 分钟                                           │
│ 关键思路:每加一个学生 = malloc 一个 Node → 链到尾节点之后   │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7

# 4.0 灵魂三问

🎯 在敲键盘之前,停 2 分钟问自己三个灵魂问题——回答清楚了再写代码,能少走 80% 的弯路。

❓ 问题一:为什么用链表不用数组?

来看反例——99% C 语言新手的直觉:

// ❌ 反例 1:固定数组
Student students[100];  // 最多 100 个学生
int count = 0;
students[count++] = newStu;  // 第 101 个学生就溢出
1
2
3
4

问题:数组大小是编译期固定的——你无法预知将来会有多少个学生。

再看"改良"后的反例——有些同学会说"那我用动态数组":

// ❌ 反例 2:动态数组 + realloc(看起来聪明,实则更危险)
Student *students = malloc(10 * sizeof(Student));  // 初始 10 个
int capacity = 10;
int count = 0;

// 满了就扩容
if (count >= capacity) {
    capacity *= 2;
    Student *tmp = realloc(students, capacity * sizeof(Student));
    if (tmp == NULL) {
        free(students);
        // ⚠️ realloc 失败时原内存已被释放!数据全丢了
        return;
    }
    students = tmp;
}
students[count++] = newStu;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

问题:

  1. realloc 扩容失败时,原内存可能已丢失(取决于实现)
  2. 每次扩容要复制全部数据,O(n) 复杂度
  3. 扩容时机(1.5 倍?2 倍?)需要精心设计

✅ 正确做法:链表——每个学生一个节点,节点头尾相连。永远不怕溢出,因为每次只 malloc 一个节点的内存,O(1) 插入,无需扩容。

方案 插入复杂度 溢出风险 代码复杂度
固定数组 Student[100] O(1) ⚠️ 100 上限 低
动态数组 + realloc 均摊 O(1),扩容时 O(n) ❌ realloc 失败的雪崩 高
链表(本案例) O(1) 尾插 ✅ 永不溢出 中

❓ 问题二:malloc 分配的内存放在哪里?忘记 free 会怎样?

答:堆(heap)——和栈上的局部变量不同,malloc 返回的内存不会随着函数返回而失效。这就是为什么 InputStudent() 里 malloc 的节点,回到 main 里还能用。

来看反例——"我以为局部变量能返回":

// ❌ 反例:栈上创建节点——函数返回后内存被回收
Node* InputStudent_WRONG() {
    Node node;             // 栈变量
    node.pNext = NULL;
    scanf("%d", &node.stu.stuNo);
    return &node;          // ⚠️ 返回栈变量地址 = 悬垂指针
}  // node 被销毁,调用方拿到的是垃圾地址
1
2
3
4
5
6
7

✅ 正确做法:malloc 分配在堆上,函数返回后地址依然有效——但你必须记住配对 free。

忘记 free 的后果不是"程序偶尔崩",而是程序跑久了 OOM:

// ❌ 反例:典型的内存泄漏场景
while (1) {
    Node *p = (Node *)malloc(sizeof(Node));
    // 忘了 free(p);
    // 每次循环泄漏 56 字节,10000 次 = 560KB —— 服务器跑一周直接 OOM
}
1
2
3
4
5
6

🔑 C 语言铁律:一次 malloc 必须对应一次 free——没有 GC,全靠自觉。

❓ 问题三:第一步做什么?先定义数据还是先写功能?

答:先定义 Student 结构体 + Node 节点结构体——先把"数据长什么样"定下来,再写操作。

来看反例——先写功能后补数据:

// ❌ 反例:直接写 InputStudent(),数据字段想到一个补一个
void InputStudent() {
    char name[20]; scanf(...);   // 先存临时变量?
    int age; scanf(...);
    // 写到哪?没有定义数据结构!边写边补字段 → 混乱
}
1
2
3
4
5
6

✅ 正确做法:"被包含的类型"先定义。Student 被 Node 包含 → Student 先定义。Node 被链表操作 → Node 第二位。这就是所有头文件都遵循"基础类型在上、复合类型在下"的顺序。

🔑 三问连起来的领悟:"想清楚 5 分钟,胜过调 bug 5 小时"——这不是教条,是真实 C 语言工程师每天在做的事。

# 4.1 定义学生结构体

新建 StudentManager.h:

#ifndef STUDENT_MANAGER_H
#define STUDENT_MANAGER_H

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

// 学生结构体
typedef struct {
    char  name[20];    // 姓名
    char  sex[4];      // 性别
    int   age;         // 年龄
    int   stuNo;       // 学号
    int   score;       // 成绩
} Student;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 4.2 定义链表节点

// 链表节点
typedef struct tagNode {
    Student         stu;      // 学生信息
    struct tagNode *pNext;    // ⭐ 指向下一个节点的指针
} Node;

// 全局头指针——整个链表从这里开始
extern Node *g_pHead;

#endif // STUDENT_MANAGER_H
1
2
3
4
5
6
7
8
9
10

# 4.3 链表尾插实现

新建 StudentManager.c:

#include "StudentManager.h"

Node *g_pHead = NULL;   // 头指针初始为空

// 链表尾插:找到最后一个节点,把新节点接上去
void addNodeToList(Node *pNewNode) {
    if (g_pHead == NULL) {
        g_pHead = pNewNode;
        return;
    }
    // 遍历到链表末尾
    Node *p = g_pHead;
    while (p->pNext != NULL) {
        p = p->pNext;
    }
    p->pNext = pNewNode;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

🔑 为什么头指针是全局变量? C 语言没有 C++ 的"类 + 成员变量"机制。全局头指针是最直接的"通讯录容器"——所有函数都能访问它。

# 4.4 录入学生信息

void InputStudent() {
    // ⭐ malloc:在堆上分配一个 Node
    Node *pNewNode = (Node *)malloc(sizeof(Node));
    if (pNewNode == NULL) {
        printf("内存分配失败!\n");
        return;
    }
    // ⚠️ 必须初始化 pNext = NULL,否则是野指针
    pNewNode->pNext = NULL;

    // 录入信息
    printf("请输入学生姓名: ");
    scanf("%19s", pNewNode->stu.name);
    printf("请输入性别(男/女): ");
    scanf("%3s", pNewNode->stu.sex);
    printf("请输入年龄: ");
    scanf("%d", &pNewNode->stu.age);
    printf("请输入学号: ");
    scanf("%d", &pNewNode->stu.stuNo);
    printf("请输入成绩: ");
    scanf("%d", &pNewNode->stu.score);

    // 尾插到链表
    addNodeToList(pNewNode);
    printf(">>> 学生信息录入成功!\n");
    pauseProgram();
    clearScreen();
}
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

🧪 在 case 1 中调用并测试:

case 1:
    InputStudent();
    break;
1
2
3

# 4.5 测试添加

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

操作:选 1 → 录入张三 → 选 1 → 录入李四 → 看到两条"录入成功"。

预期交互(你的输入用 > 标记):

***************************************
*   欢迎使用学生管理系统 v1.0         *
***************************************
*   1.录入学生信息                    *
*   ...
*   0.退出系统                        *
***************************************
请选择: > 1
请输入学生姓名: > 张三
请输入性别(男/女): > 男
请输入年龄: > 20
请输入学号: > 1001
请输入成绩: > 85
>>> 学生信息录入成功!
按回车键继续...

> [回车]

请选择: > 1
请输入学生姓名: > 李四
请输入性别(男/女): > 男
请输入年龄: > 22
请输入学号: > 1002
请输入成绩: > 90
>>> 学生信息录入成功!
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

然后选 2 打印——看到两条记录:

学号    姓名    性别    年龄    成绩
-----------------------------------------
1001    张三    男      20      85
1002    李四    男      22      90
-----------------------------------------
1
2
3
4
5

✅ 看到两条格式化记录 = 链表尾插 + 遍历打印 都通了。如果只看到一条,检查 addNodeToList 是否每次都在末尾 pNext 挂了新节点。

┌─ 📌 阶段 ② Step 2.1 小结 ─────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • 用 typedef struct 定义复合数据类型                        │
│   • malloc 在堆上分配内存(对比栈变量的生命周期)              │
│   • 链表尾插(遍历到末尾 + 接上新节点)                       │
│   • pNext = NULL 初始化 —— 忘记这行 = 野指针 segfault        │
│                                                             │
│ 💡 本阶段最大领悟:                                            │
│   "C语言的容器 = 你手写的链表;C语言的 new = malloc +        │
│    手动初始化;C语言的 delete = free + 手动置 NULL"          │
│                                                             │
│ ⚠️ 最容易踩的坑:                                             │
│   • scanf 缓冲区残留 → §04 陷阱预警已提示                     │
│   • 忘记 pNext = NULL → 遍历时死循环或 segfault             │
│   • 只添不删 → 内存泄漏(下节 Valgrind 演示)                │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 05.显示学生

void PrintStudent() {
    clearScreen();
    Node *p = g_pHead;
    if (p == NULL) {
        printf("系统中暂无学生信息。\n");
        pauseProgram();
        return;
    }
    printf("学号\t姓名\t性别\t年龄\t成绩\n");
    printf("-----------------------------------------\n");
    while (p != NULL) {
        printf("%d\t%s\t%s\t%d\t%d\n",
               p->stu.stuNo, p->stu.name,
               p->stu.sex, p->stu.age, p->stu.score);
        p = p->pNext;
    }
    printf("-----------------------------------------\n");
    pauseProgram();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

🧪 测试:添加 2 个学生 → 选 2 打印 → 看到两条格式化的记录。


# 06.删除学生

┌─ 🎯 本阶段核心 ────────────────────────────────────┐
│ 删除链表节点 + 必须 free —— 否则 Valgrind 会告你         │
│ ⚠️ 本节会用 Valgrind 演示内存泄漏现场                   │
└──────────────────────────────────────────────────────┘
1
2
3
4

# 6.0 灵魂两问

🎯 敲删除代码前,先回答这两个问题——它们的答案直接决定你会不会泄漏内存。

❓ 问题一:为什么删除后必须 free?不 free 会怎样?

答:malloc 的内存在堆上,不会自动回收。只把节点从链表上摘下来(改指针),但不 free,这块内存就永远泄漏了——进程退出前再也不会被用到。

来看反例——"看起来删掉了,其实内存还在":

// ❌ 反例:只改指针不 free——"假删除"
void delete_WRONG(Node *head, int stuNo) {
    Node *p = head;
    Node *prev = NULL;
    while (p != NULL) {
        if (p->stu.stuNo == stuNo) {
            if (prev == NULL) {
                head = p->pNext;
            } else {
                prev->pNext = p->pNext;
            }
            // ❌ 没有 free(p)!
            // p 指向的 56 字节永远回不来了
            return;  // 看起来返回了,实际上泄漏了
        }
        prev = p;
        p = p->pNext;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

用 Valgrind 运行这段代码后:

==12345== LEAK SUMMARY:
==12345==    definitely lost: 56 bytes in 1 blocks  ← 泄漏确认
1
2

✅ 正确做法:改完指针后必须 free(p)。记住 C 语言的 malloc/free 配对铁律。

❓ 问题二:头节点 vs 中间节点 vs 尾节点的删除逻辑一样吗?为什么需要 beforeNode?

答:不一样。区别在于"前驱节点怎么跳过被删节点":

删除位置 前驱节点存在吗? 需要改什么? 操作
头节点 ❌ 不存在 g_pHead 全局指针 g_pHead = p->pNext; free(p);
中间节点 ✅ 存在 prev->pNext prev->pNext = p->pNext; free(p);
尾节点 ✅ 存在 prev->pNext prev->pNext = NULL; free(p);

来看反例——"用单指针删头节点":

// ❌ 反例:用户传的是 head 的拷贝,改了没用
Node *p = g_pHead;
if (p->stu.stuNo == stuNo) {
    p = p->pNext;  // ⚠️ 只改了局部变量 p,g_pHead 纹丝不动
    free(g_pHead); // ⚠️ 野指针!g_pHead 还指向已释放的内存
}
1
2
3
4
5
6

✅ 正确做法:头节点需要直接改 g_pHead(或用二级指针),中间/尾节点改 prev->pNext。

🔑 这就是为什么代码里用全局变量 g_pHead——C 语言没有引用,二级指针是最干净的"改调用者指针"方式。另一种方案是传 Node **ppHead:

// ✅ 另一种写法:用二级指针避免区分头/中/尾
void deleteByNo(Node **ppHead, int stuNo) {
    Node *p = *ppHead;
    Node *prev = NULL;
    while (p != NULL) {
        if (p->stu.stuNo == stuNo) {
            if (prev == NULL) {
                *ppHead = p->pNext;  // 直接改调用者的指针
            } else {
                prev->pNext = p->pNext;
            }
            free(p);
            return;
        }
        prev = p;
        p = p->pNext;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这种二级指针写法在 Linus Torvalds 的口头禅里很常见——"好程序员用一级指针,伟大的程序员用二级指针"。

# 6.1 三种删除场景

// ⚠️ 先看 Valgrind 演示——故意不 free!
void DeleteStudent_LEAK() {
    int stuNo;
    printf("请输入要删除的学号: ");
    scanf("%d", &stuNo);

    Node *p = g_pHead;
    Node *prev = NULL;

    while (p != NULL) {
        if (p->stu.stuNo == stuNo) {
            // 找到了,但先不 free——让 Valgrind 抓现行
            if (prev == NULL) {
                g_pHead = p->pNext;  // 删除头节点
            } else {
                prev->pNext = p->pNext;  // 删除中间/尾节点
            }
            // ❌ 故意注释掉 free(p)
            printf(">>> 删除成功!(但内存泄漏了)\n");
            return;
        }
        prev = p;
        p = p->pNext;
    }
    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

🧪 Valgrind 验证泄漏:

# 先确认 valgrind 已安装(macOS: brew install valgrind 或用 leaks 替代)
valgrind --leak-check=full ./student_sys
1
2

操作:添加张三(学号 1001)→ 删除张三 → 退出。

预期 Valgrind 输出(关键行):

==12345== HEAP SUMMARY:
==12345==     in use at exit: 56 bytes in 1 blocks
==12345==   total heap usage: 2 allocs, 1 frees, 1,080 bytes allocated
==12345==
==12345== 56 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x...: malloc (in ...)
==12345==    by 0x...: InputStudent (StudentManager.c:23)
==12345==    by 0x...: main (main.c:30)
==12345==
==12345== LEAK SUMMARY:
==12345==    definitely lost: 56 bytes in 1 blocks  ← ⚠️ 泄漏确认!
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==      possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 0 bytes in 0 blocks
1
2
3
4
5
6
7
8
9
10
11
12
13
14

关键解读:

  • total heap usage: 2 allocs, 1 frees — malloc 了 2 次(一个 Node + 一个文件名缓冲区),只 free 了 1 次
  • definitely lost: 56 bytes — sizeof(Node) = 56 字节永久泄漏
  • InputStudent (StudentManager.c:23) — 泄漏来源文件 + 行号

# 6.2 实现删除(修复版)

void DeleteStudent() {
    int stuNo;
    printf("请输入要删除的学号: ");
    scanf("%d", &stuNo);

    Node *p = g_pHead;
    Node *prev = NULL;

    while (p != NULL) {
        if (p->stu.stuNo == stuNo) {
            if (prev == NULL) {
                g_pHead = p->pNext;      // 删除头节点
            } else {
                prev->pNext = p->pNext;  // 删除中间/尾
            }
            free(p);  // ⭐ 修复:释放内存
            printf(">>> 删除成功!\n");
            pauseProgram();
            return;
        }
        prev = p;
        p = p->pNext;
    }
    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

# 6.3 Valgrind 验证无泄漏

valgrind --leak-check=full ./student_sys
# All heap blocks were freed -- no leaks are possible ✅
1
2
┌─ 📌 阶段 ② Step 2.4 小结 ─────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • 链表三种位置删除的不同处理(头/中/尾)                     │
│   • malloc/free 配对铁律 —— 一次 malloc 一次 free            │
│   • Valgrind 抓内存泄漏 —— C 程序员的第二个编译器              │
│   • 二级指针修改调用者指针的思想                               │
│                                                             │
│ 💡 本阶段最大领悟:                                            │
│   "C语言的 'delete' 就是 free——编译器不会替你检查。           │
│    Valgrind 是你唯一的朋友"                                  │
│                                                             │
│ ⚠️ 接下来要做的(§07-08):                                   │
│   • 查找/统计/修改 —— 继续操作链表,但不再涉及 malloc/free   │
│   • 注意:修改时是覆盖字段,不是新建节点                      │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 07.查找与统计

// 查找学生
void FindStudent() {
    int stuNo;
    printf("请输入要查找的学号: ");
    scanf("%d", &stuNo);

    Node *p = g_pHead;
    int found = 0;
    while (p != NULL) {
        if (p->stu.stuNo == stuNo) {
            printf("学号\t姓名\t性别\t年龄\t成绩\n");
            printf("%d\t%s\t%s\t%d\t%d\n",
                   p->stu.stuNo, p->stu.name,
                   p->stu.sex, p->stu.age, p->stu.score);
            found = 1;
            break;
        }
        p = p->pNext;
    }
    if (!found) printf("未找到该学号的学生!\n");
    pauseProgram();
}

// 统计人数
void CountStudent() {
    int count = 0;
    Node *p = g_pHead;
    while (p != NULL) {
        count++;
        p = p->pNext;
    }
    printf("当前学生总人数: %d\n", count);
    pauseProgram();
}
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

# 08.修改学生

void ModifyStudent() {
    int stuNo;
    printf("请输入要修改的学号: ");
    scanf("%d", &stuNo);

    Node *p = g_pHead;
    while (p != NULL) {
        if (p->stu.stuNo == stuNo) {
            printf("当前信息: %s %s %d %d\n",
                   p->stu.name, p->stu.sex,
                   p->stu.age, p->stu.score);
            printf("请输入新姓名: "); scanf("%19s", p->stu.name);
            printf("请输入新性别: "); scanf("%3s", p->stu.sex);
            printf("请输入新年龄: "); scanf("%d", &p->stu.age);
            printf("请输入新成绩: "); scanf("%d", &p->stu.score);
            printf(">>> 修改成功!\n");
            pauseProgram();
            return;
        }
        p = p->pNext;
    }
    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
┌─ 📌 阶段 ② 小结 ────────────────────────────────────────┐
│ ✅ 你刚刚完成了链表 CRUD 全流程(这是 C 语言最硬核的实战):      │
│   • Create: malloc + 尾插 → 堆上分配 56 字节                  │
│   • Read:   遍历链表 → while(p){ printf; p=p->pNext; }      │
│   • Update: 查找到 → 直接修改结构体字段(覆盖而非新增)         │
│   • Delete: 三种位置删除 + free 修复泄漏 → Valgrind 验证      │
│                                                             │
│ 🔑 你亲眼看见的现象:                                          │
│   • 忘记 pNext=NULL → 野指针 → segfault                     │
│   • 删除只改指针不 free → Valgrind "definitely lost"        │
│   • 动态数组 vs 链表的取舍 → 链表 O(1) 插入、零扩容           │
│                                                             │
│ ⏸ 还没碰的(下阶段才会做):                                   │
│   • 文件读写——让数据在程序退出后还能活                         │
│   • fwrite 陷阱——存指针 = 存悬垂指针                         │
│                                                             │
│ 📌 进入下阶段前务必:                                         │
│   git add . &amp;&amp; git commit -m "stage2: linked list CRUD"    │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 09.文件持久化

┌─ 🎯 阶段 ③ 目标 ────────────────────────────────────┐
│ 完成什么:保存链表到文件 + 启动时加载文件到链表             │
│ 不做什么:不做文件校验、不做备份                            │
│ 验收标准:添加学生 → 保存 → 退出 → 重新启动 → 数据还在    │
│ 预计耗时:30 分钟                                          │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6

# 9.1 二进制保存

void SaveToFile() {
    FILE *pFile = fopen("stuinfo.dat", "wb");  // wb = 二进制写
    if (pFile == NULL) {
        printf("无法打开文件!\n");
        return;
    }
    Node *p = g_pHead;
    while (p != NULL) {
        // ⭐ 只写 Student 数据,不写 pNext 指针
        fwrite(&p->stu, sizeof(Student), 1, pFile);
        p = p->pNext;
    }
    fclose(pFile);
    printf(">>> 数据保存成功!(%s)\n", "stuinfo.dat");
    pauseProgram();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 9.2 启动时加载

void LoadFromFile() {
    FILE *pFile = fopen("stuinfo.dat", "rb");
    if (pFile == NULL) {
        printf("未找到数据文件,将创建新通讯录。\n");
        return;
    }
    // 先清空现有链表
    while (g_pHead != NULL) {
        Node *temp = g_pHead;
        g_pHead = g_pHead->pNext;
        free(temp);
    }

    Student tempStu;
    while (fread(&tempStu, sizeof(Student), 1, pFile) == 1) {
        Node *pNewNode = (Node *)malloc(sizeof(Node));
        pNewNode->stu = tempStu;
        pNewNode->pNext = NULL;
        addNodeToList(pNewNode);
    }
    fclose(pFile);
    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

在 main 开头调用 LoadFromFile(),确保每次启动自动加载。

🧪 测试持久化:

# 第一次运行:添加 → 保存 → 退出
./student_sys
# 选 1 → 添加张三(1001) → 选 3 → 保存 → 选 0 → 退出

# 检查文件是否生成
ls -la stuinfo.dat
cat stuinfo.dat  # 二进制乱码正常

# 第二次运行:启动 → 打印
./student_sys
# 选 2 → 应看到张三的记录 ✅
1
2
3
4
5
6
7
8
9
10
11

预期输出:

# 第一次启动
>>> 未找到数据文件,将创建新通讯录。
# ... 添加张三 ...
>>> 数据保存成功!(stuinfo.dat)

# 第二次启动
>>> 数据加载成功!
# 选 2 打印 → 张三的记录依然在
1
2
3
4
5
6
7
8

✅ "关闭程序再打开数据还在"——这是你写的第一个持久化程序。在这一步之前,所有数据都是"程序退出就消失"的。

# 9.3 为什么不用 fwrite(sizeof(Node))

⚠️ 经典错误:

// ❌ 反例:存整个 Node
fwrite(p, sizeof(Node), 1, pFile);
1
2

Node 结构体里有一个 pNext 指针——指针的值是内存地址。这次运行时地址是 0x7fff1234,下次运行时地址完全不同。读回来的 pNext 是一个无效的悬垂指针。更危险的是——程序大概率不会立刻崩溃,而是会在某次遍历时 segfault,调试极难定位。

✅ 正确做法:只存 Student 数据,加载时重新构建链表。

┌─ 📌 阶段 ③ 小结 ────────────────────────────────────────┐
│ ✅ 你刚刚掌握了:                                              │
│   • fwrite/fread 二进制文件读写                              │
│   • "不存指针,只存数据" 的反序列化核心原则                   │
│   • 启动时清空旧链表再加载——避免重复数据                     │
│   • 启停数据不丢失——你的第一个"持久化"程序                    │
│                                                             │
│ 💡 本阶段最大领悟:                                            │
│   "fwrite 不懂你的数据结构——你得自己决定存什么、不存什么"      │
│                                                             │
│ ⚠️ 这个坑咬过无数 C 程序员:                                  │
│   fwrite(&amp;node, sizeof(Node)...) → 看似"方便",              │
│   实则把运行时地址写入文件 → 加载回来全是垃圾 →               │
│   这就是为什么你永远只存 Student,不存 Node                   │
└──────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 10.项目总结

# 10.1 整体架构

main.c ──→ showMenu() ──→ switch(choice) ──→ 各功能函数
                   ↑                              │
                   └──────── 循环 ────────────────┘

StudentManager.h/c ──→ Student 结构体
                   ──→ Node 链表节点(全局 g_pHead)
                   ──→ CRUD 函数 + 文件 IO
1
2
3
4
5
6
7

# 10.2 核心知识点覆盖

C语言知识点 代码体现 掌握标志
struct + typedef Student 结构体、Node 节点 能解释为何 Student 先于 Node 定义
malloc/free 动态创建/释放链表节点 Valgrind 输出 "no leaks are possible"
指针 p->pNext、&p->stu.age 能区分 -> 和 . 的使用场景
链表 尾插、遍历、三种位置删除 能手画三种删除场景的指针变化
文件 IO fopen/fwrite/fread/fclose 能解释为何不存 pNext 指针
printf/scanf 菜单、格式化输出、用户输入 能处理 scanf 缓冲区残留问题
流程控制 while/switch-case 菜单循环 能写出"选 0 退出"的完整分支
函数封装 每个功能独立函数 能解释为何不把所有代码写 main 里

# 11.技术思考:C vs C++

同一套"学生通讯录",用 C 语言和 C++ 分别实现,差异立现:

维度 C 语言实现(本案例) C++ 实现(同案例 C++ 版)
容器 手写链表 + 全局头指针 g_pHead std::vector<Student> — 一行替代 50 行链表代码
内存管理 malloc/free 手动配对,Valgrind 检查 构造函数/析构函数自动(RAII),编译器保证
数据封装 struct + 独立函数,全部 public class + 成员函数 + private 隐藏
文件 IO fopen/fwrite/fread,二进制原始字节 ifstream/ofstream,类型安全
字符串 定长 char[20],溢出即 UB std::string 自动扩容,安全
代码量 ~400 行 ~300 行
内存安全 依赖 Valgrind 检查 编译器 + RAII 保证
并发安全 ❌ 无,全局变量天然不安全 可用 mutex + lock_guard

# 代码对比:同一功能两种实现

删除学生(C 语言版):

void DeleteStudent() {
    int stuNo; scanf("%d", &stuNo);
    Node *p = g_pHead, *prev = NULL;
    while (p != NULL) {
        if (p->stu.stuNo == stuNo) {
            if (prev == NULL) g_pHead = p->pNext;
            else prev->pNext = p->pNext;
            free(p);  // ⚠️ 手动释放——忘了就泄漏
            return;
        }
        prev = p; p = p->pNext;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

删除学生(C++ 等效版):

void DeleteStudent(vector<Student>& students) {
    int stuNo; cin >> stuNo;
    auto it = find_if(students.begin(), students.end(),
                      [stuNo](auto& s){ return s.stuNo == stuNo; });
    if (it != students.end()) {
        students.erase(it);  // ✅ 自动释放内存
        cout << "删除成功!\n";
    }
}
1
2
3
4
5
6
7
8
9

删 10 行 C 代码 → 1 行 students.erase(it)。这就是 STL 容器 + RAII 的价值。

学完本章后翻到 C++ 版的同一案例——你会直观感受到 STL 容器和 RAII 替你省掉了多少样板代码。

# 延伸挑战

完成主流程后,尝试把下列五项自己加上。建议按顺序完成——每道挑战都建立在上一道的基础上。


挑战 1(基础)· 改用双向链表 —— 删除不需 prev 变量

给 Node 加上 pPrev 指针:

// ✅ 双向链表节点
typedef struct tagDNode {
    Student         stu;
    struct tagDNode *pPrev;  // ⭐ 前驱指针
    struct tagDNode *pNext;  // 后继指针
} DNode;
1
2
3
4
5
6

预期效果:删除时不再需要遍历维护 prev 变量,直接 p->pPrev->pNext = p->pNext。

// ✅ 双向链表删除——不需要 prev
void deleteDNode(DNode *p) {
    if (p->pPrev) p->pPrev->pNext = p->pNext;
    if (p->pNext) p->pNext->pPrev = p->pPrev;
    free(p);
}
1
2
3
4
5
6

完成后再用 Valgrind 验证——双向链表每个节点多占 8 字节(pPrev 指针),但删除逻辑简化了 50%。


挑战 2(进阶)· 改用动态数组 + realloc —— 亲历"扩容陷阱"

把你的链表换成动态数组,实现 add/delete/find:

// 动态数组版通讯录
Student *g_students = NULL;  // 动态数组
int      g_capacity = 0;     // 容量
int      g_count = 0;        // 当前人数

void addStudent_DynArray(Student stu) {
    if (g_count >= g_capacity) {
        int newCap = g_capacity == 0 ? 4 : g_capacity * 2;
        Student *tmp = realloc(g_students, newCap * sizeof(Student));
        if (tmp == NULL) {
            printf("扩容失败,数据可能丢失!\n");
            return;
        }
        g_students = tmp;
        g_capacity = newCap;
    }
    g_students[g_count++] = stu;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

⚠️ 重点体会:

  1. 删除时需要 memmove 前移元素 —— 比链表的 O(1) 指针改动重很多
  2. realloc 失败时原内存状态 —— 不同实现行为不同(POSIX 下原内存不变,Windows 下已释放)
  3. 扩容时机 —— 2 倍增长 vs 1.5 倍增长,直接影响内存碎片率

挑战 3(进阶)· 加成绩排序和学号去重

用 qsort 按成绩排序后打印:

int compareByScore(const void *a, const void *b) {
    const Student *s1 = (const Student *)a;
    const Student *s2 = (const Student *)b;
    return s2->score - s1->score;  // 降序
}

void PrintSortedByScore() {
    int count = getCount();
    Student *arr = (Student *)malloc(count * sizeof(Student));
    // 从链表拷贝到数组
    Node *p = g_pHead;
    for (int i = 0; i < count; i++) {
        arr[i] = p->stu;
        p = p->pNext;
    }
    qsort(arr, count, sizeof(Student), compareByScore);
    // 打印排序后的结果...
    free(arr);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

再添加学号去重检查——InputStudent() 中录入前扫描链表,学号已存在则拒绝。


挑战 4(进阶)· 加数据校验和 CSV 文本格式存储

给成绩添加范围检查(0-100),学号格式检查(1000-9999):

// 成绩校验
int score;
do {
    printf("请输入成绩(0-100): ");
    scanf("%d", &score);
} while (score < 0 || score > 100);
1
2
3
4
5
6

再把文件持久化从二进制 (fwrite) 改成 CSV 文本格式:

// ✅ CSV 保存——人类可读,跨平台
void SaveToCSV() {
    FILE *f = fopen("stuinfo.csv", "w");
    fprintf(f, "学号,姓名,性别,年龄,成绩\n");
    Node *p = g_pHead;
    while (p != NULL) {
        fprintf(f, "%d,%s,%s,%d,%d\n",
                p->stu.stuNo, p->stu.name,
                p->stu.sex, p->stu.age, p->stu.score);
        p = p->pNext;
    }
    fclose(f);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

对比二进制 vs CSV:二进制省空间但不可读,CSV 可读但字段包含逗号时需引号转义。真实项目中 CSV 更常见(因为 git diff 能看懂)。


挑战 5(终极)· 移植到 C++ —— 亲自丈量"从 C 到 C++ 的距离"

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

模块 C 版本(本案例) C++ 版本(同章节 C++ 篇)
容器 手写链表 ~60 行 std::vector ~2 行
内存管理 malloc/free 分散各处 RAII 自动管理
文件 IO fwrite/fread 二进制 ifstream/ofstream
字符串 char[20] 定长 std::string
菜单分派 switch-case 可升级为函数指针表

🎯 挑战 5 的价值:亲手走一遍 C → C++ 的迁移,你会精准理解 RAII、STL、封装替你省掉的不是"打字量",而是"bug 出现概率"——忘记 free 的 bug 在 C++ 中根本不会编译出来。

上次更新: 2026/06/10, 11:13:41
README
银行账户管理系统

← README 银行账户管理系统→

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