第三十讲:综合实战——用函数与作用域规则重构一个学生成绩管理系统
好,终于到了最后一讲。说实话,前面二十九讲我们啃了不少硬骨头——指针、数组、作用域、递归……今天咱们来点实在的:用前面学过的所有知识,把一个“能跑就行”的学生成绩管理系统,重构成一个结构清晰、可维护、可扩展的工程。
我当年刚入行时,第一个项目就是写这种管理系统。那时候年轻,一个 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. 知识体系总览
下面这张图,把本章涉及的核心知识点串起来了。你可以把它当作一张“重构地图”。
6. 重构带来的改变
重构完之后,你会发现几个明显的变化:
- main 函数从 60 行降到 20 行——读代码的人一眼就能看出程序在做什么
- 每个函数不超过 15 行——出 bug 了定位极快
- static 关键字保护了内部数据——其他文件想乱改?没门
- 添加新功能不需要改已有函数——比如加个“按姓名查找”,写一个新函数就行
总结一句话:函数是代码的骨架,作用域是代码的边界。骨架正了,边界清了,代码自然就稳了。
好了,三十讲的内容到这里就全部结束了。希望你能把这些规则用到实际项目里——哪怕只是写一个几十行的小工具,也试着拆成函数、控制好作用域。养成习惯后,你会感谢自己的。
公众号:蓝海资料掘金营,微信deep3321