2,112
社区成员
本篇文章以学生成绩管理系统
为例,带大家入门课程设计。
写一个工程,最重要的就是要知道:我要写一个什么东西?
举一个现实中的例子。bilibili是一个视频网站,而如今有很多产品经理批评bilibili的定位不清晰,功能混杂。所以方向歪了,之后走的每一步都很艰难。
我们第一个要考虑的问题是:学生成绩管理系统是什么?它的功能是什么?它能做什么,它不能做什么?
那么我们可以从哪些渠道来知道它的功能呢?
本文作为一篇指南,只实现最基本的功能,即学生成绩的增删改查(CRUD)
。实际上,学生成绩管理系统的功能还有很多,比如:登录模块、权限管理模块、成绩分析模块等等。这些功能都可以在本文的基础上进行扩展。分工时可以考虑按照模块进行分工。
这一小节,笔者带大家设计一个简单的学生成绩管理系统。
设计方案是一个分工编写代码之前的必要步骤。小组各成员应按照设计方案进行编码,这样可以保证各成员编写的代码可以互相调用(即统一函数名等),而不是各自为战。
设计时,可以按照"实体对象"的概念进行设计。如这个系统中,有学生
、课程
、成绩
三个实体。那么我们要为每一个实体建立一个结构体
。如果有登录模块,那么还需要一个用户
实体。
那么一个学生
结构体应该怎么设计呢?参考现实情况,有姓名、学号、性别、年龄等属性。那么我们就可以这样设计:
#include <stdbool.h>
struct Student{
int id;
char[20] name;
bool gender; // bool类型在<stdbool.h>中
Birthday birthday; // Birthday是一个结构体,包含年月日
};
确定好"对象"之后,我们再确定"对象之间的联系"。如:
即做成表格的话,它应该是长这样的:
学生 | 课程 | 成绩 |
---|---|---|
张三 | 语文 | 90 |
张三 | 数学 | 80 |
李四 | 语文 | 70 |
... | ... | ... |
当然,我们应该在学生一栏中填写学号 ,课程一栏中填写课程号 。 |
填写学号的原因是:学号是唯一的,而姓名不一定唯一。
填写课程号的原因是:字符串不方便操作,数字好操作。(课程名字不可以重复,不然学生就分不清啦)
这样,基本的设计就结束了。
首先,我们把三个实体定义出来。定义结构体部分的代码写在struct.h
中,也可以分成三个文件,分别定义student.h
、course.h
、score.h
。本文采用后者。
// student.h
#include <stdbool.h>
typedef struct Student{
int id;
char[20] name;
bool gender; // bool类型在<stdbool.h>中
Birthday birthday; // Birthday是一个结构体,包含年月日
} Student;
问题:要不要在结构体中加入课程
和成绩
呢?即:
// student.h
#include <stdbool.h>
typedef struct Student{
int id;
char[20] name;
bool gender;
Birthday birthday;
Course_Score_Record[20] records;
} Student;
答案:否。看似合理,但是这样做就把其他两个实体和学生实体耦合
在一起了。这样做的后果是:如果我们要修改课程或者成绩的结构体,那么就要修改学生结构体,维护起来很麻烦。我们要设计三个实体及他们之间的关系,不能合在一起。
// course.h
typedef struct Course{
int id;
char[50] name;
int credit; // 学分
int hours; // 课时
} Course;
问题:要不要在结构体中加入成绩
呢?
// score.h
typedef struct Score{
int student_id;
int course_id;
int score;
} Score;
设计结构体时,千万要分清结构体之间的界限,多加思考。程序员的常态是思考两小时,编码五分钟!
"实体对象"设计成结构体,对他们的操作可以设计成函数。我们已知四种操作:增删改查。那么显然,我们要对三个实体都要有增删改查的操作,最少要有12个函数。下面以查成绩为例,讲解如何设计函数。
如:我要查询学号为20236000
的高数1
成绩。那么我就可以设计一个函数:
// function.h
Score* getScore(int studentID, int courseID);
如果我不知道怎么设计,那么可以思考一下我要如何用这个函数。如:我要查询学号为20236000
的高数1
成绩,那么可以进行如下思考:
我需要输入的信息(Input):学生和课程
我需要输出的信息(Output):成绩我如何表示我的输入信息:学号和课程号
我如何表示我的输出信息:成绩实体(因为没有给成绩设计id,但也可以设计一个id)查询过程中需要用到哪些额外的信息:score表
score表需要通过参数的形式传进来吗:不需要,通过文件访问就行。
把上述信息写成c语言,就是:
// function.h
#include "student.h"
#include "course.h"
#include "score.h"
Score* score = getScore(int studentID, int courseID);
// function.c
#include "function.h"
Score* getScore(int studentID, int courseID){
FILE* fp = fopen("score.csv", "r");
Score* score = (Score*)malloc(sizeof(Score));
while(fscanf(fp, "%d,%d,%d", &score->student_id, &score->course_id, &score->score) != EOF){
if(score->student_id == studentID && score->course_id == courseID){
return score;
}
}
fclose(fp);
return NULL;
}
上述代码中fscanf(...)
体现了使用课程号的优点。如果使用课程名,那么读取就不会这么简单了。fscanf("%d, %s, %d", &score->student_id, score->course_name, &score->score)
是错误滴。
我们注意到查询成绩时使用了文件读写。正常情况下,所有的数据都是存在硬盘里的,我们需要通过文件读写来访问数据。但是,如果数据量很大,那么文件读写的效率就会很低。所以,我们可以把数据读到内存中,然后在内存中进行操作,最后再写回硬盘。这样做的好处是:内存的读写速度比硬盘快很多,所以可以提高程序的运行效率。而这个读入内存的操作,可以在打开程序时进行,称之为初始化
。而写回文件通常是在进行更改后立马进行,这是因为如果程序崩溃了,且在修改之后没有保存的话,那么数据就丢失了。所以一般是这样的:
// main.c
int main(){
init(); // 将硬盘里的东西(即文件里的数据)读入内存
while(true){
// do something
save(); // 将内存中的数据写回硬盘
}
}
有了初始化之后,就可以重写查询函数了
// function.c
extern Score* scores; // scores是一个全局变量,存储了所有的成绩,在init()中初始化
Score* getScore(int studentID, int courseID){
for(int i = 0; i < score_count; i++){
if(scores[i].student_id == studentID && scores[i].course_id == courseID){
return &scores[i];
}
}
return NULL;
}
同样,设计函数时也要注意分清各个函数之间的界限。保持函数之间的独立性(即低耦合性),为一个动作设计一个函数。如:查询成绩的函数,不应该包含修改成绩的功能。如果给出了学生姓名,则应该设计一个函数,根据学生姓名查询学生id。然后再调用查询成绩的函数。
编码就是实战步骤了。考虑到C课程设计是各位同学们的第一个课程设计,所以笔者不在这里列出太多对代码规范要求。只列出一点点,剩余的部分,需要在接下来的课程设计中逐渐完善。
student
、stu
、word2vec()
(这里2=to,常见的还有4=for)等。不建议使用拼音,不会的单词查一查就好了。student.c
中,把课程的增删改查写在course.c
中,把成绩的增删改查写在score.c
中。malloc()
后要free()
fopen()
后要fclose()
以下是对未来的期待
自己的代码要测试后才能交给别人。测试时,要测试各种边界情况。如:输入的学号不存在、输入的课程号不存在、输入的成绩不合法等等。当然,也要测试正确情况。
初期,可以新开一个工程,把要测试的部分代码复制过来,然后进行测试。然后要逐渐学习测试框架,可以使用单元测试框架,如gtest
、cppunit
等,这些工具都是有助于提升测试效率的。
要学会单步调试、断点调试、查看内存、查看寄存器等操作。学习使用assert,#DEBUG等宏。初期,可以使用printf来查看是否执行正确,不过建议逐渐舍弃这种方法(但是printf真的香啊,就是后期清理起来比较麻烦,到处都是printf)
如果你是用QQ群传文件,那么代码合并大概率会成为一个令人头疼的环节。因为组长手里的版本和组员手里的版本可能不一致。而git就可以很好的缓解这个问题。
代码合并阶段一定要耐心,首先确定是否正确引入头文件,检查是否有冲突的宏定义,检查是否有重复的函数名等等。基本上就是运行一下,改一下,运行一下,改一下……
这个阶段全组要全程参与,因为不一定谁的代码出bug了……
以上就是一个课设的基本流程。下面链接里有关于student部分的源码,可以参考一下。感谢大家的阅读。
链接:https://download.csdn.net/download/m0_55717883/88179607 可免费下载