前面我们已经学习了:
变量和数据类型
输入输出
运算符
判断
循环
函数
数组
...
到这里,真正缺少的不是新的语法,而是:
把这些知识组合起来。
所以这一章不再以“学习一个新语法”为主,而是通过几个实际项目:
需求
↓
设计数据
↓
写函数
↓
拆分模块
↓
保存文件
↓
编译
↓
调试
完成一个真正可以运行的 C 程序。
一、本章项目路线
建议按照难度逐渐增加:
项目 1:猜数字
↓
项目 2:学生管理系统
↓
项目 3:课程表管理系统
三个项目分别锻炼:
猜数字
↓
基础语法 + 函数 + 循环
学生管理系统
↓
数组 + 字符串 + 结构体 + 文件
课程表管理系统
↓
结构体 + 动态内存 + 文件
+ 多文件 + Makefile
+ 完整项目设计
二、项目 1:猜数字
这是最简单的综合项目。
项目需求
程序随机生成一个数字:
1 ~ 100
然后用户不断输入:
你猜一个数字
程序告诉用户:
太大
太小
猜对了
例如:
请输入数字:50
太小
请输入数字:75
太大
请输入数字:63
猜对了!
三、猜数字需要哪些知识
变量
↓
scanf
↓
if
↓
while
↓
函数
↓
随机数
可以进一步练习:
统计猜了多少次
四、猜数字基础代码
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main() {
srand((unsigned int)time(NULL));
int target = rand() % 100 + 1;
int guess;
int count = 0;
while (1) {
printf("请输入数字: ");
scanf("%d", &guess);
count++;
if (guess > target) {
printf("太大了\n");
}
else if (guess < target) {
printf("太小了\n");
}
else {
printf("猜对了!\n");
printf("你一共猜了 %d 次\n", count);
break;
}
}
return 0;
}五、猜数字升级
可以加入:
难度
├─ 简单:1 ~ 50
├─ 普通:1 ~ 100
└─ 困难:1 ~ 1000
还可以:
限制猜测次数
例如:
最多 10 次
也可以增加:
重新开始
退出
这就开始有“小程序”的感觉了。
六、项目 2:学生管理系统
这是本章第一个真正重要的项目。
项目目标:
添加学生
删除学生
修改学生
查询学生
显示所有学生
保存学生
读取学生
退出程序
七、学生的数据结构
前面已经学过结构体。
可以定义:
typedef struct {
int id;
char name[50];
int age;
double score;
} Student;一个学生:
Student
├─ id
├─ name
├─ age
└─ score
八、学生管理系统的数据
最简单版本可以:
Student students[100];但是:
最多只能 100 人
为了练习动态内存,可以进一步:
Student *students;然后:
students = malloc(sizeof(Student) * capacity);这样就可以在运行时决定容量。
九、学生管理系统的功能
推荐设计:
1. 添加学生
2. 删除学生
3. 修改学生
4. 查询学生
5. 显示全部
6. 保存数据
7. 读取数据
0. 退出
十、菜单
程序启动:
========== 学生管理系统 ==========
1. 添加学生
2. 删除学生
3. 修改学生
4. 查询学生
5. 显示全部
6. 保存数据
7. 读取数据
0. 退出
请输入选择:
这里主要使用:
printf
scanf
switch
while
十一、添加学生
函数:
void add_student(Student *students, int *count);基本逻辑:
判断是否还有空间
↓
输入 id
↓
输入 name
↓
输入 age
↓
输入 score
↓
count++
十二、为什么 count 要传指针
例如:
int count = 0;函数:
void add_student(Student *students, int *count)然后:
(*count)++;因为我们要修改:
main 中的 count
这正好应用了之前学的:
指针
+
函数参数
十三、显示学生
void print_students(Student *students, int count)使用:
for遍历:
students[0]
students[1]
students[2]输出:
ID 姓名 年龄 成绩
1 Alice 18 95.5
2 Bob 19 88.0
十四、查询学生
可以按照:
ID
查询。
例如:
Student *find_student(
Student *students,
int count,
int id
);找到后:
return &students[i];找不到:
return NULL;这又同时使用了:
结构体指针
+
函数返回值
+
NULL
十五、修改学生
先:
输入 ID
然后:
find_student()
找到:
Student *
然后修改:
student->name
student->age
student->score十六、删除学生
删除一个数组元素:
找到目标
↓
后面的元素全部向前移动
↓
count--
例如:
A B C D
删除:
B
变成:
A C D
代码思想:
for (int i = index; i < count - 1; i++) {
students[i] = students[i + 1];
}这会再次用到:
结构体
+
数组
+
for
十七、保存文件
学生数据不能只存在内存。
程序退出以后:
students
↓
消失
所以需要:
保存
↓
students.dat
例如:
fwrite(
students,
sizeof(Student),
count,
fp
);十八、读取文件
程序启动时:
students.dat
↓
fread
↓
students
于是:
上次的数据
↓
本次启动继续使用
这就是:
数据持久化。
十九、学生管理系统文件结构
到这里就不能再把所有代码全部堆在 main.c。
推荐:
student_manager/
│
├─ main.c
│
├─ student.c
├─ student.h
│
├─ file.c
├─ file.h
│
├─ config.h
│
└─ Makefile
二十、student.h
#ifndef STUDENT_H
#define STUDENT_H
typedef struct {
int id;
char name[50];
int age;
double score;
} Student;
void add_student(
Student *students,
int *count
);
void print_students(
Student *students,
int count
);
Student *find_student(
Student *students,
int count,
int id
);
void delete_student(
Student *students,
int *count
);
void modify_student(
Student *students,
int count
);
#endif这里放:
结构体
+
函数声明
二十一、file.h
#ifndef FILE_H
#define FILE_H
#include "student.h"
int save_students(
const char *filename,
Student *students,
int count
);
int load_students(
const char *filename,
Student *students,
int *count
);
#endif二十二、main.c
main.c 主要负责:
菜单
流程控制
调用模块
例如:
#include <stdio.h>
#include "student.h"
#include "file.h"
int main() {
Student students[100];
int count = 0;
while (1) {
printf("\n");
printf("1. 添加学生\n");
printf("2. 删除学生\n");
printf("3. 修改学生\n");
printf("4. 查询学生\n");
printf("5. 显示全部\n");
printf("6. 保存\n");
printf("7. 读取\n");
printf("0. 退出\n");
int choice;
scanf("%d", &choice);
switch (choice) {
case 1:
add_student(students, &count);
break;
case 2:
delete_student(students, &count);
break;
case 3:
modify_student(students, count);
break;
case 5:
print_students(students, count);
break;
case 6:
save_students(
"students.dat",
students,
count
);
break;
case 7:
load_students(
"students.dat",
students,
&count
);
break;
case 0:
return 0;
}
}
}这里没有实现具体功能。
这正是多文件编程的意义:
main.c
↓
负责“调用谁”
而:
student.c
↓
负责“学生功能怎么实现”
二十三、学生管理系统升级:动态内存
把:
Student students[100];升级为:
Student *students;然后:
int capacity = 10;
int count = 0;
students = malloc(
sizeof(Student) * capacity
);如果满了:
count == capacity
↓
realloc
↓
扩大空间
例如:
Student *temp = realloc(
students,
sizeof(Student) * capacity * 2
);
if (temp != NULL) {
students = temp;
capacity *= 2;
}这样就形成了:
结构体
+
指针
+
malloc
+
realloc
+
free
二十四、项目 3:课程表管理系统
这个项目非常适合拿来作为最终项目。
因为你之前已经实际做过课程表。
目标:
课程添加
课程删除
课程修改
查询课程
显示课程表
保存课程表
读取课程表
二十五、课程数据结构
可以设计:
typedef struct {
char name[100];
char teacher[100];
char room[100];
int day;
int period;
} Course;代表:
课程名称
教师
教室
星期
第几节
例如:
C语言
张老师
A101
星期一
第1节
二十六、课程表的数据
如果固定:
7 天
6 节
可以:
Course courses[7][6];但是如果课程数量不固定:
Course *courses;
再配合:
malloc
realloc
free
就可以做动态课程列表。
二十七、课程表菜单
========== 课程表管理 =========
1. 添加课程
2. 删除课程
3. 修改课程
4. 查询课程
5. 显示课程表
6. 保存课程表
7. 读取课程表
0. 退出
二十八、显示课程表
例如:
第1节 第2节 第3节
星期一 C语言 数学 英语
星期二 物理 计算机 体育
星期三 ...
这里主要使用:
二维数组
for
结构体
字符串
二十九、课程冲突检查
这是一个很有意思的功能。
例如:
星期一
第1节
已经有:
C语言
那么再次添加:
数学
星期一
第1节
就应该提示:
课程时间冲突!
逻辑:
遍历现有课程
↓
比较 day
↓
比较 period
↓
相同?
↓
冲突
三十、课程表文件保存
可以使用二进制:
fwrite(
courses,
sizeof(Course),
count,
fp
);也可以保存成文本。
例如:
C语言|张老师|A101|1|1
数学|李老师|B202|1|2
三十一、进一步:JSON
如果以后希望:
C 程序
↓
生成 JSON
↓
网页
↓
手机 App
那么可以设计:
{
"name": "C语言",
"teacher": "张老师",
"room": "A101",
"day": 1,
"period": 1
}不过:
JSON 并不是 C 标准库自带的文件格式。
需要自己写 JSON 生成/解析代码,或者使用第三方 JSON 库。
当前基础课程不必马上深入。
三十二、最终项目架构
如果把课程表做到比较完整,可以:
course_manager/
│
├─ main.c
│
├─ course.c
├─ course.h
│
├─ file.c
├─ file.h
│
├─ config.h
│
├─ Makefile
│
└─ data/
└─ courses.dat
三十三、course.h
#ifndef COURSE_H
#define COURSE_H
typedef struct {
char name[100];
char teacher[100];
char room[100];
int day;
int period;
} Course;
void add_course(
Course **courses,
int *count,
int *capacity
);
void delete_course(
Course *courses,
int *count
);
void modify_course(
Course *courses,
int count
);
void print_courses(
Course *courses,
int count
);
Course *find_course(
Course *courses,
int count,
int day,
int period
);
#endif这里已经用到了:
二级指针
因为:
Course **courses可能需要修改:
courses
本身指向的地址,例如 realloc() 后指针可能发生变化。
这正好是之前指针学习成果的最终应用之一。
三十四、文件模块
可以:
file.h
↓
save_courses()
load_courses()
file.c
↓
真正实现文件操作
这样主程序完全不用关心:
fopen
fread
fwrite
fclose
三十五、配置模块
例如:
#ifndef CONFIG_H
#define CONFIG_H
#define DEFAULT_CAPACITY 16
#define COURSE_NAME_SIZE 100
#define TEACHER_NAME_SIZE 100
#define ROOM_NAME_SIZE 100
#endif这样修改配置的时候:
只改 config.h
三十六、Makefile
最终项目就可以使用:
CC = gcc
CFLAGS = -Wall -Wextra -g
TARGET = course_manager
OBJS = main.o course.o file.o
$(TARGET): $(OBJS)
$(CC) $(CFLAGS) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
.PHONY: clean
clean:
rm -f $(TARGET) $(OBJS)
编译:
make清理:
make clean三十七、调试最终项目
项目越大,越容易产生 Bug。
建议:
普通问题
↓
printf
编译问题
↓
-Wall -Wextra
内存问题
↓
AddressSanitizer
崩溃
↓
GDB
Linux 内存检查
↓
Valgrind
例如:
gcc -Wall -Wextra -g \
-fsanitize=address,undefined \
main.c course.c file.c \
-o course_manager三十八、项目开发流程
以后不要一上来就写几千行代码。
推荐:
① 确定需求
↓
② 设计数据结构
↓
③ 设计函数
↓
④ 先实现最简单功能
↓
⑤ 测试
↓
⑥ 拆成 .h / .c
↓
⑦ 加入文件保存
↓
⑧ 加入动态内存
↓
⑨ 加入错误处理
↓
⑩ Makefile
↓
⑪ 调试
三十九、不要一次写完整项目
例如学生管理系统。
不要:
一天写完 2000 行
推荐:
第一阶段
↓
显示菜单
第二阶段
↓
添加学生
第三阶段
↓
显示学生
第四阶段
↓
查询
第五阶段
↓
修改 / 删除
第六阶段
↓
文件保存
第七阶段
↓
动态内存
第八阶段
↓
拆分多文件
每一步都先运行。
四十、测试
一个程序不是:
能运行
就算完成。
需要测试:
正常输入
↓
边界输入
↓
错误输入
↓
极端输入
↓
重复操作
↓
空数据
↓
文件不存在
↓
文件损坏
例如学生管理系统:
0 个学生
1 个学生
100 个学生
超过容量
重复 ID
不存在的 ID
空文件
不存在的文件
四十一、最终项目应该具备什么
一个合格的综合项目至少应该:
✓ 能正常运行
✓ 有清晰的数据结构
✓ 使用函数拆分功能
✓ 使用结构体组织数据
✓ 正确使用指针
✓ 正确管理动态内存
✓ 能保存数据
✓ 能读取数据
✓ 使用 .h / .c 模块化
✓ 使用 Makefile 构建
✓ 能处理基本错误
✓ 能调试
四十二、综合项目能力对应表
学习内容 项目中的应用
──────────────────────────────────────
变量 保存状态
printf / scanf 输入输出
if / switch 菜单判断
for / while 遍历和菜单循环
函数 功能拆分
数组 保存多个对象
字符串 姓名、课程名等
指针 修改数据、传递地址
二级指针 realloc 等场景
结构体 Student / Course
malloc 动态数据
realloc 扩容
free 释放内存
文件操作 数据持久化
#define 配置
.h / .c 模块化
extern / static 模块管理
Makefile 自动构建
GDB 调试
Sanitizer 内存错误检查
四十三、本章推荐的三个项目难度
Level 1
└─ 猜数字
Level 2
└─ 学生管理系统
├─ 添加
├─ 删除
├─ 修改
├─ 查询
├─ 文件保存
└─ 文件读取
Level 3
└─ 课程表管理系统
├─ 结构体
├─ 动态内存
├─ 时间冲突
├─ 文件持久化
├─ 多文件
├─ Makefile
└─ 调试
四十四、最终练习:从零完成课程表系统
不要直接复制现成代码。
自己完成:
Step 1
设计 Course 结构体
Step 2
设计菜单
Step 3
添加课程
Step 4
显示课程
Step 5
查询课程
Step 6
修改课程
Step 7
删除课程
Step 8
检查时间冲突
Step 9
保存文件
Step 10
读取文件
Step 11
使用 malloc
Step 12
使用 realloc
Step 13
使用 free
Step 14
拆成 .h / .c
Step 15
写 Makefile
Step 16
使用 Sanitizer / GDB 调试
最终达到:
运行程序
↓
操作课程表
↓
关闭程序
↓
重新打开
↓
课程仍然存在
这才真正完成了:
一个完整的 C 语言项目。
四十五、完整课程结束
至此,这套 C 语言基础课程结束了
Comments NOTHING