第30章 综合项目实战:学生成绩管理系统

终于到了最后一章。说实话,写到这里我还有点舍不得。

前面29章,我们聊了SQLite的方方面面,从安装到API,从事务到性能优化。但学了一身功夫,总得找个地方练练手。这一章,我们就拿一个经典项目——学生成绩管理系统——来做个完整的实战。

这个项目我做过不下五遍。最早是在大学课程设计里,用文件读写硬撸了一个。后来工作中给培训机构做过一个类似的,用SQLite存储,C语言实现。嗯,踩过的坑不少,今天一并分享给你。

30.1 需求分析:别急着写代码

我见过太多人,拿到需求就开始敲键盘。结果写到一半发现表结构不对,又回头改。浪费的时间,够你喝三杯咖啡了。

先想清楚:这个系统要干什么?

  • 学生信息管理:增删改查学生基本信息(学号、姓名、班级)
  • 课程信息管理:增删改查课程(课程编号、课程名、学分)
  • 成绩录入与查询:录入学生各科成绩,支持按学生或按课程查询
  • 统计功能:计算平均分、最高分、最低分、及格率
  • 数据持久化:所有数据存到SQLite数据库,程序退出不丢失

说白了,就是一个轻量级的教务系统。不需要网络,不需要多用户,单机跑就行。

我的习惯: 写代码前,先在纸上画一遍数据流。哪张表先建,哪个字段是外键,心里有数了再动手。

30.2 表设计:三张表搞定

这个项目用三张表就够了。我最初设计时想过用四张,后来发现有一张是多余的。你想想看,学生和课程是多对多的关系,成绩表就是它们的关联表。

表名字段说明
studentsid, student_id, name, class学生信息表
coursesid, course_id, name, credit课程信息表
scoresid, 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)
);
注意: 我在项目中遇到过一个问题——同一个学生同一门课录了两次成绩。后来加了UNIQUE约束,才彻底解决。这个坑,你千万别踩。

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;
}
关键点: 这里用了INSERT OR REPLACE,意思是如果同一个学生同一门课已经录过成绩,就覆盖更新。避免了重复录入的麻烦。

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 系统架构图

下面这张图,是我画这个系统时最常用的结构。你看一眼,整个系统的脉络就清楚了。

学生成绩管理系统架构图 主菜单 学生管理 课程管理 成绩管理 增删改查学生信息 增删改查课程信息 录入/查询/统计 SQLite 数据库 (students / courses / scores)

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;
}
小技巧: 实际项目中,我会把输入验证加进去。比如学号不能为空,成绩必须在0-100之间。别偷懒,用户输入什么妖魔鬼怪都有可能。

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
我曾经踩过的坑: 有一次我忘了加 -lpthread,结果程序在Linux上跑起来就崩。查了半天,才发现SQLite在某些操作下需要线程支持。嗯,从那以后我编译时都会加上这个参数。

30.7 总结

这个项目虽然不大,但五脏俱全。你学会了:

  • 怎么从需求出发设计表结构
  • 怎么用C语言操作SQLite实现CRUD
  • 怎么把程序打包发布给别人用

说实话,这个系统我后来给一个培训机构做过定制版,加了导出Excel的功能,卖了五千块。你想想看,一个学生成绩管理系统,能值这么多钱?其实不是代码值钱,是解决问题的能力值钱。

好了,30章的内容到这里就结束了。希望这一路走来,你收获的不只是技术,还有解决问题的思路和信心。

公众号:蓝海资料掘金营,微信deep3321