第三十讲:综合实战——用函数与作用域规则重构一个学生成绩管理系统

好,终于到了最后一讲。说实话,前面二十九讲我们啃了不少硬骨头——指针、数组、作用域、递归……今天咱们来点实在的:用前面学过的所有知识,把一个“能跑就行”的学生成绩管理系统,重构成一个结构清晰、可维护、可扩展的工程。

我当年刚入行时,第一个项目就是写这种管理系统。那时候年轻,一个 main 函数写了八百行,变量满天飞。后来被 mentor 骂了一顿,才老老实实学重构。嗯,今天这堂课,就当是我把当年踩过的坑,提前帮你填上。

1. 原始代码的问题分析

先看一段典型的“新手代码”。它功能上没问题,但你看完就知道为什么需要重构了。

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

int main() {
    char names[50][20];
    float scores[50];
    int count = 0;
    int choice;
    float sum = 0, avg;
    int i, j;
    char temp[20];
    float temp_score;

    while (1) {
        printf("1. 添加学生 2. 显示所有 3. 排序 4. 退出\n");
        scanf("%d", &choice);

        if (choice == 1) {
            printf("输入姓名: ");
            scanf("%s", names[count]);
            printf("输入成绩: ");
            scanf("%f", &scores[count]);
            count++;
        } else if (choice == 2) {
            sum = 0;
            for (i = 0; i < count; i++) {
                printf("%s: %.1f\n", names[i], scores[i]);
                sum += scores[i];
            }
            if (count > 0) {
                avg = sum / count;
                printf("平均分: %.2f\n", avg);
            }
        } else if (choice == 3) {
            for (i = 0; i < count - 1; i++) {
                for (j = 0; j < count - 1 - i; j++) {
                    if (scores[j] < scores[j + 1]) {
                        temp_score = scores[j];
                        scores[j] = scores[j + 1];
                        scores[j + 1] = temp_score;
                        strcpy(temp, names[j]);
                        strcpy(names[j], names[j + 1]);
                        strcpy(names[j + 1], temp);
                    }
                }
            }
            printf("排序完成\n");
        } else if (choice == 4) {
            break;
        }
    }
    return 0;
}
⚠ 问题清单:
  • 所有逻辑挤在 main 里,没有函数拆分
  • 全局变量和局部变量混用,sum、avg 在循环外定义,容易误用
  • 排序代码直接操作全局数组,耦合度高
  • 没有错误处理(比如 count 超过 50)
  • 变量命名随意(i、j、temp)

说白了,这段代码就像一间堆满杂物的房间——东西都能找到,但想挪个沙发得先把整个屋子搬空。

2. 重构目标与模块划分

重构不是重写。我们要做的是:保持功能不变,把代码拆成职责清晰的函数,并严格控制变量的作用域

我个人习惯把这类系统分成三层:

  • 数据层:管理学生数组,提供增、删、查、改接口
  • 业务层:计算平均分、排序、统计等逻辑
  • 展示层:菜单交互、输入输出

你想想看,这样一拆,以后想加个“按姓名查找”功能,只需要在数据层加一个函数,展示层加一个菜单项,业务层完全不用动。

核心原则:每个函数只做一件事,变量只在需要它的最小范围内定义。

3. 重构后的代码结构

下面是我重构后的版本。注意看每个函数的作用域设计。

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

#define MAX_STUDENTS 50
#define NAME_LEN 20

// ========== 数据层 ==========
typedef struct {
    char name[NAME_LEN];
    float score;
} Student;

static Student students[MAX_STUDENTS];
static int student_count = 0;

int add_student(const char *name, float score) {
    if (student_count >= MAX_STUDENTS) {
        return -1;  // 满了
    }
    strcpy(students[student_count].name, name);
    students[student_count].score = score;
    student_count++;
    return 0;
}

int get_student_count(void) {
    return student_count;
}

const Student* get_student(int index) {
    if (index < 0 || index >= student_count) {
        return NULL;
    }
    return &students[index];
}

// ========== 业务层 ==========
float calculate_average(void) {
    if (student_count == 0) return 0.0f;
    float sum = 0.0f;
    for (int i = 0; i < student_count; i++) {
        sum += students[i].score;
    }
    return sum / student_count;
}

void sort_by_score_desc(void) {
    // 冒泡排序,只操作静态数组
    for (int i = 0; i < student_count - 1; i++) {
        for (int j = 0; j < student_count - 1 - i; j++) {
            if (students[j].score < students[j + 1].score) {
                Student temp = students[j];
                students[j] = students[j + 1];
                students[j + 1] = temp;
            }
        }
    }
}

// ========== 展示层 ==========
void print_all_students(void) {
    if (student_count == 0) {
        printf("暂无学生数据\n");
        return;
    }
    for (int i = 0; i < student_count; i++) {
        printf("%s: %.1f\n", students[i].name, students[i].score);
    }
    printf("平均分: %.2f\n", calculate_average());
}

void show_menu(void) {
    printf("\n===== 学生成绩管理系统 =====\n");
    printf("1. 添加学生\n");
    printf("2. 显示所有学生\n");
    printf("3. 按成绩降序排序\n");
    printf("4. 退出\n");
    printf("请选择: ");
}

int main() {
    int choice;
    char name[NAME_LEN];
    float score;

    while (1) {
        show_menu();
        scanf("%d", &choice);

        if (choice == 1) {
            printf("输入姓名: ");
            scanf("%s", name);
            printf("输入成绩: ");
            scanf("%f", &score);
            if (add_student(name, score) != 0) {
                printf("错误:学生已满!\n");
            } else {
                printf("添加成功\n");
            }
        } else if (choice == 2) {
            print_all_students();
        } else if (choice == 3) {
            sort_by_score_desc();
            printf("排序完成\n");
        } else if (choice == 4) {
            break;
        } else {
            printf("无效选项\n");
        }
    }
    return 0;
}

4. 作用域规则的具体应用

这个重构版本里,作用域规则体现在三个地方:

作用域类型 示例 说明
文件作用域(static) static Student students[MAX_STUDENTS] 只在当前 .c 文件可见,外部无法直接访问
块作用域 for (int i = 0; ...) 中的 i 循环结束后 i 自动销毁,不会污染外部
函数参数作用域 add_student(const char *name, float score) 参数只在函数内部有效,不影响外部同名变量
💡 避坑指南:我曾经在一个项目里把学生数组定义成全局非 static 的,结果另一个模块不小心用 extern 引用了它,直接修改了内部数据,查了两天才找到 bug。从那以后,所有模块内部的数据我都加 static。

5. 知识体系总览

下面这张图,把本章涉及的核心知识点串起来了。你可以把它当作一张“重构地图”。

学生成绩管理系统重构 —— 知识体系 原始代码:main 函数 800 行,变量满天飞 重构目标:函数拆分 + 作用域控制 数据层 static 数组 + 增删查改接口 业务层 平均分、排序、统计 展示层 菜单、输入输出、错误提示 最终成果:可维护、可扩展、低耦合

6. 重构带来的改变

重构完之后,你会发现几个明显的变化:

  • main 函数从 60 行降到 20 行——读代码的人一眼就能看出程序在做什么
  • 每个函数不超过 15 行——出 bug 了定位极快
  • static 关键字保护了内部数据——其他文件想乱改?没门
  • 添加新功能不需要改已有函数——比如加个“按姓名查找”,写一个新函数就行
总结一句话:函数是代码的骨架,作用域是代码的边界。骨架正了,边界清了,代码自然就稳了。

好了,三十讲的内容到这里就全部结束了。希望你能把这些规则用到实际项目里——哪怕只是写一个几十行的小工具,也试着拆成函数、控制好作用域。养成习惯后,你会感谢自己的。


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