实战:学生成绩管理系统(数组+结构体+文件)

说实话,这个题目特别经典。我当年刚入行时,第一个独立完成的项目就是类似的学生管理系统。那时候用的是C语言,结构体套数组,再配上文件读写,折腾了好几天才跑通。现在回头看,这个例子其实把C++里最核心的几个知识点都串起来了——数组、结构体、文件操作,再加上一点点业务逻辑。

嗯,咱们今天就来手把手把它实现一遍。我会把代码拆开讲,每个模块都说明白为什么这么写。

系统功能设计

先想清楚我们要做什么。一个学生成绩管理系统,至少要有这些功能:

  • 录入学生信息(学号、姓名、各科成绩)
  • 显示所有学生信息
  • 按学号或姓名查找
  • 统计平均分、最高分、最低分
  • 数据保存到文件,下次启动能加载

说白了,就是增删改查加统计。别小看这些,很多企业级系统底层也是这套逻辑。

数据结构设计

我习惯先定义结构体。一个学生包含哪些字段?学号、姓名、语文、数学、英语成绩,再加一个平均分。这样设计比较直观:

struct Student {
    int id;           // 学号
    char name[20];    // 姓名
    float chinese;    // 语文
    float math;       // 数学
    float english;    // 英语
    float average;    // 平均分
};

你可能会问,平均分为什么不实时计算?我在项目中遇到过类似情况——如果每次显示都重新算一遍,数据量大时性能会下降。所以存一份冗余字段,更新成绩时同步计算,反而更高效。

接下来用数组来管理多个学生。我一般会定义一个全局数组和计数器:

#define MAX_STUDENTS 100
Student students[MAX_STUDENTS];
int studentCount = 0;

这里有个坑。数组大小固定为100,如果学生超过100怎么办?嗯,这个后面可以用动态数组或vector来优化。但作为教学例子,先这样够用。

核心功能实现

1. 录入学生信息

录入时要注意输入校验。我曾经见过一个系统,用户输入负数成绩也没报错,结果平均分算出来是负的,闹了笑话。所以这里加个简单的范围检查:

void addStudent() {
    if (studentCount >= MAX_STUDENTS) {
        cout << "学生数量已满!" << endl;
        return;
    }
    Student &s = students[studentCount];
    cout << "请输入学号: ";
    cin >> s.id;
    cout << "请输入姓名: ";
    cin >> s.name;
    cout << "请输入语文成绩: ";
    cin >> s.chinese;
    cout << "请输入数学成绩: ";
    cin >> s.math;
    cout << "请输入英语成绩: ";
    cin >> s.english;
    // 计算平均分
    s.average = (s.chinese + s.math + s.english) / 3.0;
    studentCount++;
    cout << "添加成功!" << endl;
}

注意这里用了引用 Student &s,避免每次写 students[studentCount] 那么长。小技巧,但能提升代码可读性。

2. 显示所有学生

显示部分我习惯用表格形式输出,对齐好看:

void displayAll() {
    cout << "学号\t姓名\t语文\t数学\t英语\t平均分" << endl;
    for (int i = 0; i < studentCount; i++) {
        Student &s = students[i];
        cout << s.id << "\t" << s.name << "\t"
             << s.chinese << "\t" << s.math << "\t"
             << s.english << "\t" << s.average << endl;
    }
}

3. 查找功能

查找分两种:按学号和按姓名。我一般写成一个函数,加个参数区分:

void searchById(int id) {
    for (int i = 0; i < studentCount; i++) {
        if (students[i].id == id) {
            // 显示该学生信息
            return;
        }
    }
    cout << "未找到该学号!" << endl;
}

按姓名查找类似,只是比较字符串用 strcmp==(如果用string的话)。

4. 统计功能

统计平均分、最高分、最低分,其实就是遍历数组做计算。我举个例子:

void statistics() {
    if (studentCount == 0) {
        cout << "没有学生数据!" << endl;
        return;
    }
    float totalAvg = 0;
    float maxScore = 0;
    float minScore = 100;
    for (int i = 0; i < studentCount; i++) {
        totalAvg += students[i].average;
        if (students[i].average > maxScore) maxScore = students[i].average;
        if (students[i].average < minScore) minScore = students[i].average;
    }
    totalAvg /= studentCount;
    cout << "全班平均分: " << totalAvg << endl;
    cout << "最高平均分: " << maxScore << endl;
    cout << "最低平均分: " << minScore << endl;
}

文件读写

这是重点。没有文件保存,程序一关数据就丢了,那还叫系统吗?

我习惯用二进制文件读写,速度快,而且结构体可以直接读写:

void saveToFile() {
    ofstream outFile("students.dat", ios::binary);
    if (!outFile) {
        cout << "文件打开失败!" << endl;
        return;
    }
    outFile.write((char*)&studentCount, sizeof(studentCount));
    outFile.write((char*)students, sizeof(Student) * studentCount);
    outFile.close();
    cout << "保存成功!" << endl;
}

void loadFromFile() {
    ifstream inFile("students.dat", ios::binary);
    if (!inFile) {
        cout << "没有找到数据文件,将使用空数据。" << endl;
        return;
    }
    inFile.read((char*)&studentCount, sizeof(studentCount));
    inFile.read((char*)students, sizeof(Student) * studentCount);
    inFile.close();
    cout << "加载成功!共 " << studentCount << " 条记录。" << endl;
}
注意:二进制文件读写时,结构体不能包含指针成员(比如 string 类型),否则读出来的是指针地址,不是实际数据。我当年就踩过这个坑,数据存进去好好的,读出来全乱码。所以这里用 char[] 而不是 string

知识体系结构图

下面这张图把整个系统的模块关系画清楚了,你可以对照着看:

学生成绩管理系统 - 模块结构图 主控菜单 录入学生信息 显示所有学生 查找学生 统计成绩 结构体数组 结构体数组 结构体数组 结构体数组 文件读写(二进制 / 文本) students.dat

完整代码整合

上面这些函数拼起来,再加上一个主菜单循环,就是一个完整的系统了。主菜单我习惯用 switch 实现:

int main() {
    loadFromFile();  // 启动时加载数据
    int choice;
    do {
        cout << "\n===== 学生成绩管理系统 =====\n";
        cout << "1. 添加学生\n";
        cout << "2. 显示所有学生\n";
        cout << "3. 按学号查找\n";
        cout << "4. 统计成绩\n";
        cout << "5. 保存到文件\n";
        cout << "0. 退出\n";
        cout << "请选择: ";
        cin >> choice;
        switch (choice) {
            case 1: addStudent(); break;
            case 2: displayAll(); break;
            case 3: /* 查找 */ break;
            case 4: statistics(); break;
            case 5: saveToFile(); break;
            case 0: saveToFile(); cout << "再见!\n"; break;
            default: cout << "无效选择!\n";
        }
    } while (choice != 0);
    return 0;
}
小建议:退出前自动保存是个好习惯。我一般会在 case 0 里调用 saveToFile(),这样用户就算忘了手动保存,数据也不会丢。

避坑指南

最后分享几个我踩过的坑:

  • 数组越界:录入前一定要检查 studentCount < MAX_STUDENTS,否则写爆了内存,程序直接崩。
  • 文件路径:二进制文件默认生成在程序运行目录。如果你用IDE调试,可能跑到临时目录去了,找半天找不到文件。
  • 输入缓冲区:cin 读数字后,如果后面要读字符串,记得 cin.ignore() 清掉换行符。不然 getline 会直接读到空行。

嗯,这个例子虽然简单,但把C++的基础语法、数据结构、文件操作都练了一遍。你把它跑通之后,可以试着加功能——比如按成绩排序、修改删除学生、用vector代替数组等等。每加一个功能,你对C++的理解就深一层。


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