第一次完成项目的一些心得——学生成绩管理系统

689 阅读7分钟

1. 读取文件时要注意文件的字符编码,否则会出现乱码的情况

乱码 (2).bmp 文件采用的是utf-8编码,网上搜索后将文件编码格式改为ANSI即可

2. 尽量让每个方法的功能单一且清晰,避免无意义的代码,减少冗余,减少代码重复率。

以前我往往会喜欢使用链式编程的方式,所以这一次一开始考虑到键值合法性,用户再次确认输入主要是想使用链式编程的方式,每一次功能的使用都是方法的层层调用。

比如:
    菜单上打印功能,用户键值执行对应的功能,但是如果我现在只有三个功能,
    分别输入123,来实现,
    那么如果用户键值超出范围要怎么办?
    我一开始想到的是写一个typeInt()方法用来接收用户键值并返回,
    再写一个方法checkLeg()检查输入值是否在范围中,返回0/1用于判断是否合法的,
    又写了一个方法isUserSure()确认键值,返回值也是0/1用于表示用户是否确认。
    这样,每当用户键值,程序会先判断键值合法性,不合法则重新输入,合法再让用户确认输入,(这其中也会判断合法性)
    不确认就重输,用户确认则程序继续运行。

每一次用户的输入需要经历的流程:isUserSure(checkLeg(typrInt()))。

(然而后来才突然发现键值合法性直接用switch的default就能解决。。。)

这一次发现这样会很容易出现一些bug。例如:

1) 循环引用: 
    如当用户键值1,是否确认,键值1,是否确认,键值1…这里就是是否确认的键值又再次询问是否确认了,
    想解决这个问题倒是很简单,重新安排一下方法的调用顺序就可以了,不过这样体现方法嵌套的一个弊端。

2) 查找bug: 
    方法嵌套导致了循环复用(可能只是因为我写的不好二出现的问题),
    先说找bug的时候,因为嵌套的问题,需要设置多个节点,找bug在哪,
    由于同一段在不同的位置使用多次,这就给找什么时候出的问题,
    为了在某一以时间使用的时候不出问题,就比较麻烦,不可避免地就又要加参数,用来作某段代码的“锁”…

旧.bmp

这次写项目时看了老师的代码后大受启发,以后要尽量把程序写成这样

202252791725.bmp

202252791743.bmp

所以,用上面的思路优化了两版之后又参考老师的children的格式重新写了代码,
这一次,方法都没有返回值,方法只起到处理的作用,
从main的一开始将需要最终需要的东西定义好——Student数组,
接下来所有的方法都只是对这个student数组进行处理,
处理完成后,把结果交给writefile函数,在屏幕上打印,写入文件里。

这样写之后显而易见地代码变得清晰了非常多,各个方法的功能比起之前更加清晰了。

写起来非常清晰,比如这个功能放到哪里用,出现了bug是哪个方法的问题,都十分清晰,是链式编程比不了的。(个人看法)


2022-5-30添加

今天恰好看到了一个视频《代码整洁之道——单一职责》我觉得可以很好描述上面的体会

www.bilibili.com/video/BV1WT…

有评论这样写道:

单一职责,是一种粒度切分的做法,因为大粒度的功能职责不清的话,会导致复用低,耦合度高,代码重复,而单一职责做得好的话,粒度小的功能可以直接调用,粒度大的功能可以通过组合来实现

尽可能让程序中各个不同功能的代码能够切分开


3. switch最好使用char类型case直接‘1’,‘2’...,如果使用int类型,在有default的前提下,程序会陷入死循环。

int->char, %d->%s, 1->'1' ... case.bmp

死循环
死循环 (2).bmp

switch陷入死循环: 网上搜索得知:

scanf函数是从缓冲区接受数值
而当我们输入字母或其他时,字符就一直留在缓冲区,再次循环,
scanf再从缓冲区获取时还是字母,就相当跳过了scanf这一句,从而造成死循环。

个人理解的话和getchar()方法读缓冲区,只要不是‘\n’方法就一直等待,输入回车时再将缓冲区里的字符一次性给%s有点像,

缓冲区中的字符只有被使用才会消除,而这里switch获取的不是int,所以字符还留在缓冲区,然后这一次while结束了,因为while(1),所以又开始第二次循环,但是这次缓冲区已经有上一次留下的字符了,所以直接进行了判断,然后又相同形成循环?

那么可以知道scanf实际是检查缓冲区有没有东西,如果有,又符合条件就会直接读走,如果没有就会让键盘输入。 那如果有又不符合条件会读走,不会消除(?)


2022.5.30

今天老师让我详细说一下关于这个switch的问题,录视频的时候又想到了几个解决办法:⬇⬇⬇

先来说一个常规的:

default里面加上一个item = NULL;

default:
    item = NULL;
    printf("\n键值错误!请重新输入!\n");
    break;

这样缓冲区里的字符就被消除掉了,可以正常停止。(这个方法是我的一个同学的处理办法)

他当时跟我说的时候我没具体听,我还以为他直接清空缓冲区了,但是当时没多想,现在老师让我详细说一下这才想起来

直接网上搜索清空缓冲区的方法:

C语言:清空缓冲区 - myrj - 博客园 (cnblogs.com)

  • fflush(stdout);

    这个不知道为什么在我的vs上不奏效,不过链接里好像说跟版本有一定关系吧

  • getchar();

    default:
    getchar();
    printf("\n键值错误!请重新输入!\n");
    break;
    

    像我上面说的那样,因为以前用过,当时查过原理,所以印象很深,这里直接用getchar把缓冲区里残留的字符读出来。

  • scanf()

    bug出现的原因就是因为scanf直接把缓冲区里的字符拿过来了,是不是有了点想法?这里直接再用一个scanf(%c)再把缓冲区里的拿出来就好了。不过上面链接里给的scanf()使用的更严谨一些

    default:
    scanf("%*[^\n]"); scanf("%*c");
    printf("\n键值错误!请重新输入!\n");
    break;
    

    第一个 scanf() 将逐个读取缓冲区中\n之前的其它字符, % 后面的 * 表示将读取的这些字符丢弃,遇到\n字符时便停止读取。此时,缓冲区中尚有一个\n遗留,

    第二个 scanf() 再将这个\n读取并丢弃,这里的星号和第一个 scanf() 的星号作用相同。

    由于所有从键盘的输入都是以回车结束的,而回车会产生一个\n字符,

    所以将\n连同它之前的字符全部读取并丢弃之后,也就相当于清除了输入缓冲区。


    2022.6.1 今天一个同学说我上面提供的这些方法,除了一个scanf,其他都不管用,(c语言的不兼容居然这么离谱???)他提出了另一个解决方法:

    rewind(stdin) 网上搜索结果如下:

    函数名: rewind()

    功 能: 将文件内部的位置指针重新指向一个流(数据流/文件)的开头

    注意:不是文件指针而是文件内部的位置指针,随着对文件的读写文件的位置指针(指向当前读写字节)向后移动。而文件指针是指向整个文件,如果不重新赋值文件指针不会改变。

    rewind函数作用等同于 (void)fseek(stream, 0L, SEEK_SET); [1] 

    用 法: void rewind(FILE *stream);

    头文件: stdio.h

    返回值:无

    也就是说这个方法通过将指针重新指向标准输入流的开头来变相地达到“清空缓冲区”的效果。

    不过根据字面大概也能猜到弊端————缓冲区中仍有数据,如果缓冲区内存的数据比较多而且后续还要使用的话,最好还是直接清空一下吧。