C语言课程设计指南

Mr.Z2001 2023-08-06 22:12:59

本篇文章以学生成绩管理系统为例,带大家入门课程设计。

1. 功能确定

写一个工程,最重要的就是要知道:我要写一个什么东西?
举一个现实中的例子。bilibili是一个视频网站,而如今有很多产品经理批评bilibili的定位不清晰,功能混杂。所以方向歪了,之后走的每一步都很艰难。

我们第一个要考虑的问题是:学生成绩管理系统是什么?它的功能是什么?它能做什么,它不能做什么?

那么我们可以从哪些渠道来知道它的功能呢?

  • 看ppt
  • 上网查
  • 问老师
  • 找一个现实中的成品,看看它的功能,仿照着做一个低配版就可以了。

本文作为一篇指南,只实现最基本的功能,即学生成绩的增删改查(CRUD)。实际上,学生成绩管理系统的功能还有很多,比如:登录模块、权限管理模块、成绩分析模块等等。这些功能都可以在本文的基础上进行扩展。分工时可以考虑按照模块进行分工。

2 设计

这一小节,笔者带大家设计一个简单的学生成绩管理系统。

设计方案是一个分工编写代码之前的必要步骤。小组各成员应按照设计方案进行编码,这样可以保证各成员编写的代码可以互相调用(即统一函数名等),而不是各自为战。

设计时,可以按照"实体对象"的概念进行设计。如这个系统中,有学生课程成绩三个实体。那么我们要为每一个实体建立一个结构体。如果有登录模块,那么还需要一个用户实体。

那么一个学生结构体应该怎么设计呢?参考现实情况,有姓名、学号、性别、年龄等属性。那么我们就可以这样设计:

#include <stdbool.h>
struct Student{
  int id;
  char[20] name;
  bool gender;        // bool类型在<stdbool.h>中
  Birthday birthday;  // Birthday是一个结构体,包含年月日
};

确定好"对象"之后,我们再确定"对象之间的联系"。如:

  • 一名学生可以修多门课程
  • 一名学生的一门课程对应一个成绩

即做成表格的话,它应该是长这样的:

学生课程成绩
张三语文90
张三数学80
李四语文70
.........
当然,我们应该在学生一栏中填写学号,课程一栏中填写课程号

填写学号的原因是:学号是唯一的,而姓名不一定唯一。

填写课程号的原因是:字符串不方便操作,数字好操作。(课程名字不可以重复,不然学生就分不清啦)

这样,基本的设计就结束了。

首先,我们把三个实体定义出来。定义结构体部分的代码写在struct.h中,也可以分成三个文件,分别定义student.hcourse.hscore.h。本文采用后者。

2.1 学生设计

// 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;

答案:否。看似合理,但是这样做就把其他两个实体和学生实体耦合在一起了。这样做的后果是:如果我们要修改课程或者成绩的结构体,那么就要修改学生结构体,维护起来很麻烦。我们要设计三个实体及他们之间的关系,不能合在一起。

2.2 课程设计

// course.h
typedef struct Course{
  int id;
  char[50] name;
  int credit; // 学分
  int hours;  // 课时
} Course;

问题:要不要在结构体中加入成绩呢?

2.3 成绩设计

// score.h
typedef struct Score{
  int student_id;
  int course_id;
  int score;
} Score;

设计结构体时,千万要分清结构体之间的界限,多加思考。程序员的常态是思考两小时,编码五分钟!

2.4 操作设计

"实体对象"设计成结构体,对他们的操作可以设计成函数。我们已知四种操作:增删改查。那么显然,我们要对三个实体都要有增删改查的操作,最少要有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。然后再调用查询成绩的函数。

3 编码

编码就是实战步骤了。考虑到C课程设计是各位同学们的第一个课程设计,所以笔者不在这里列出太多对代码规范要求。只列出一点点,剩余的部分,需要在接下来的课程设计中逐渐完善。

3.1 最基本的规范

  • 缩进规范:一个tab键,或者2个空格,或者4个空格。现代IDE都有自动格式化,多多利用自动格式化,学习格式化的规范。可以安装格式化插件,如google,clang-format等。
  • 命名规范:避免abcd这样的命名。建议使用英语单词或其缩写。如:studentstuword2vec()(这里2=to,常见的还有4=for)等。不建议使用拼音,不会的单词查一查就好了。
  • 不要把什么东西都写在main函数里,要分模块编写。如:把学生的增删改查写在student.c中,把课程的增删改查写在course.c中,把成绩的增删改查写在score.c中。
  • 避免写一个超级长的函数,一般一个超级长的函数都可以拆分成多个函数。
  • 避免写一个超级长的文件。按照关联度适当拆分文件。
  • malloc()后要free()
  • fopen()后要fclose()

以下是对未来的期待

3.2 合作规范

  • 代码要写注释,注释要写清楚。注释要写在函数头部,写清楚函数的功能、输入、输出、使用方法等。
  • 要学习使用git,逐渐放弃QQ群传文件的合作方法。

4 测试/调试

自己的代码要测试后才能交给别人。测试时,要测试各种边界情况。如:输入的学号不存在、输入的课程号不存在、输入的成绩不合法等等。当然,也要测试正确情况。

初期,可以新开一个工程,把要测试的部分代码复制过来,然后进行测试。然后要逐渐学习测试框架,可以使用单元测试框架,如gtestcppunit等,这些工具都是有助于提升测试效率的。

要学会单步调试、断点调试、查看内存、查看寄存器等操作。学习使用assert,#DEBUG等宏。初期,可以使用printf来查看是否执行正确,不过建议逐渐舍弃这种方法(但是printf真的香啊,就是后期清理起来比较麻烦,到处都是printf)

5 代码合并

如果你是用QQ群传文件,那么代码合并大概率会成为一个令人头疼的环节。因为组长手里的版本和组员手里的版本可能不一致。而git就可以很好的缓解这个问题。

代码合并阶段一定要耐心,首先确定是否正确引入头文件,检查是否有冲突的宏定义,检查是否有重复的函数名等等。基本上就是运行一下,改一下,运行一下,改一下……

这个阶段全组要全程参与,因为不一定谁的代码出bug了……

6 结语

以上就是一个课设的基本流程。下面链接里有关于student部分的源码,可以参考一下。感谢大家的阅读。

链接:https://download.csdn.net/download/m0_55717883/88179607 可免费下载

...全文
359 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

2,112

社区成员

发帖
与我相关
我的任务
社区描述
东北大学计算机类专业社区
辽宁省·沈阳市
社区管理员
  • gibeonwu
  • Mr.Z2001
  • Yu_Des2023
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

自强不息,知行合一

试试用AI创作助手写篇文章吧