第30章 综合项目实战:学生成绩管理系统
终于到了最后一章。说实话,写到这里我还有点舍不得。
前面29章,我们聊了SQLite的方方面面,从安装到API,从事务到性能优化。但学了一身功夫,总得找个地方练练手。这一章,我们就拿一个经典项目——学生成绩管理系统——来做个完整的实战。
这个项目我做过不下五遍。最早是在大学课程设计里,用文件读写硬撸了一个。后来工作中给培训机构做过一个类似的,用SQLite存储,C语言实现。嗯,踩过的坑不少,今天一并分享给你。
30.1 需求分析:别急着写代码
我见过太多人,拿到需求就开始敲键盘。结果写到一半发现表结构不对,又回头改。浪费的时间,够你喝三杯咖啡了。
先想清楚:这个系统要干什么?
- 学生信息管理:增删改查学生基本信息(学号、姓名、班级)
- 课程信息管理:增删改查课程(课程编号、课程名、学分)
- 成绩录入与查询:录入学生各科成绩,支持按学生或按课程查询
- 统计功能:计算平均分、最高分、最低分、及格率
- 数据持久化:所有数据存到SQLite数据库,程序退出不丢失
说白了,就是一个轻量级的教务系统。不需要网络,不需要多用户,单机跑就行。
30.2 表设计:三张表搞定
这个项目用三张表就够了。我最初设计时想过用四张,后来发现有一张是多余的。你想想看,学生和课程是多对多的关系,成绩表就是它们的关联表。
| 表名 | 字段 | 说明 |
|---|---|---|
| students | id, student_id, name, class | 学生信息表 |
| courses | id, course_id, name, credit | 课程信息表 |
| scores | id, student_id, course_id, score | 成绩表,外键关联前两张表 |
建表SQL如下:
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
class TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS courses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
course_id TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
credit REAL NOT NULL
);
CREATE TABLE IF NOT EXISTS scores (
id INTEGER PRIMARY KEY AUTOINCREMENT,
student_id TEXT NOT NULL,
course_id TEXT NOT NULL,
score REAL NOT NULL,
FOREIGN KEY (student_id) REFERENCES students(student_id),
FOREIGN KEY (course_id) REFERENCES courses(course_id),
UNIQUE(student_id, course_id)
);
30.3 核心逻辑:C语言全功能实现
代码结构我习惯分成三个文件:
db.h— 数据库操作的头文件db.c— 数据库操作的具体实现main.c— 主程序,菜单交互
这样分层,逻辑清晰,后期维护也方便。我曾经在一个项目里把所有代码塞到一个文件里,三千行,找bug找到怀疑人生。从那以后,我再也不这么干了。
30.3.1 数据库初始化
int init_db(const char *db_path) {
sqlite3 *db;
int rc = sqlite3_open(db_path, &db);
if (rc != SQLITE_OK) {
fprintf(stderr, "打开数据库失败: %s\n", sqlite3_errmsg(db));
return -1;
}
const char *sql_students =
"CREATE TABLE IF NOT EXISTS students (...);";
const char *sql_courses =
"CREATE TABLE IF NOT EXISTS courses (...);";
const char *sql_scores =
"CREATE TABLE IF NOT EXISTS scores (...);";
char *err_msg = NULL;
sqlite3_exec(db, sql_students, 0, 0, &err_msg);
sqlite3_exec(db, sql_courses, 0, 0, &err_msg);
sqlite3_exec(db, sql_scores, 0, 0, &err_msg);
sqlite3_close(db);
return 0;
}
30.3.2 添加学生
int add_student(sqlite3 *db, const char *sid, const char *name, const char *class) {
const char *sql = "INSERT INTO students (student_id, name, class) VALUES (?, ?, ?);";
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, sid, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, name, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 3, class, -1, SQLITE_STATIC);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (rc == SQLITE_DONE) ? 0 : -1;
}
30.3.3 录入成绩
int add_score(sqlite3 *db, const char *sid, const char *cid, double score) {
const char *sql = "INSERT OR REPLACE INTO scores (student_id, course_id, score) VALUES (?, ?, ?);";
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, sid, -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, cid, -1, SQLITE_STATIC);
sqlite3_bind_double(stmt, 3, score);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
return (rc == SQLITE_DONE) ? 0 : -1;
}
30.3.4 查询学生成绩
void query_student_scores(sqlite3 *db, const char *sid) {
const char *sql =
"SELECT c.name, s.score FROM scores s "
"JOIN courses c ON s.course_id = c.course_id "
"WHERE s.student_id = ?;";
sqlite3_stmt *stmt;
sqlite3_prepare_v2(db, sql, -1, &stmt, NULL);
sqlite3_bind_text(stmt, 1, sid, -1, SQLITE_STATIC);
printf("课程名\t\t成绩\n");
printf("----------------------\n");
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char *cname = sqlite3_column_text(stmt, 0);
double score = sqlite3_column_double(stmt, 1);
printf("%s\t\t%.1f\n", cname, score);
}
sqlite3_finalize(stmt);
}
30.4 系统架构图
下面这张图,是我画这个系统时最常用的结构。你看一眼,整个系统的脉络就清楚了。
30.5 主程序与菜单交互
主程序就是一个循环,用户选什么功能,就调什么函数。我习惯用switch-case,清晰明了。
int main() {
sqlite3 *db;
init_db("student.db");
sqlite3_open("student.db", &db);
int choice;
do {
printf("\n===== 学生成绩管理系统 =====\n");
printf("1. 添加学生\n");
printf("2. 添加课程\n");
printf("3. 录入成绩\n");
printf("4. 查询学生成绩\n");
printf("5. 统计课程成绩\n");
printf("0. 退出\n");
printf("请选择: ");
scanf("%d", &choice);
switch(choice) {
case 1: {
char sid[20], name[50], cls[20];
printf("学号: "); scanf("%s", sid);
printf("姓名: "); scanf("%s", name);
printf("班级: "); scanf("%s", cls);
add_student(db, sid, name, cls);
break;
}
case 3: {
char sid[20], cid[20];
double score;
printf("学号: "); scanf("%s", sid);
printf("课程编号: "); scanf("%s", cid);
printf("成绩: "); scanf("%lf", &score);
add_score(db, sid, cid, score);
break;
}
// 其他case类似,不一一列举
}
} while(choice != 0);
sqlite3_close(db);
return 0;
}
30.6 打包发布:让程序跑在别人机器上
写完了代码,怎么给别人用?总不能让人家装个编译环境吧。
我一般用静态编译,把SQLite的库直接编进可执行文件里。这样用户拿到一个exe就能跑。
# 静态编译(Linux/macOS)
gcc -o student_manager main.c db.c -lsqlite3 -static
# Windows下用MinGW
gcc -o student_manager.exe main.c db.c -lsqlite3 -static-libgcc
如果你用的是SQLite的合并文件(sqlite3.c),那就更简单了:
gcc -o student_manager main.c db.c sqlite3.c -lpthread -ldl
30.7 总结
这个项目虽然不大,但五脏俱全。你学会了:
- 怎么从需求出发设计表结构
- 怎么用C语言操作SQLite实现CRUD
- 怎么把程序打包发布给别人用
说实话,这个系统我后来给一个培训机构做过定制版,加了导出Excel的功能,卖了五千块。你想想看,一个学生成绩管理系统,能值这么多钱?其实不是代码值钱,是解决问题的能力值钱。
好了,30章的内容到这里就结束了。希望这一路走来,你收获的不只是技术,还有解决问题的思路和信心。