• C博客作业06


    第六次C博客作业


    Q1.本章学习总结

    1.1 学习内容总结

    • 结构体如何定义,成员如何赋值
      • 首先要定义一个结构体,常用的方法是
         struct 结构名
         {
              结构成员
         };    //注意这里有个分号
    
    • 然后需要在主函数中声明结构体变量,可在声明时对逐一结构成员进行赋初值,也可以采用 结构体变量名.结构成员名= 的方式进行赋值
    • 如果结构成员是char类型的,记得使用strcpy()函数进行赋值
         struct student
         {
              int num;
              int score;
         };   //这里分号前可以直接写上结构体变量名,用逗号分隔,这样在下面使用不用先声明
    
         int main()
         {
              struct student p = { 10,80 };   //声明时直接按成员顺序逐个赋值
              p.num = 10;     //中间用点连接,不能用->
              p.score = 80;
         }
    
    • 我们现在更支持使用typedef来定义结构体,和普通的定义基本一致,而且可以改变每次声明新的变量时都要写的struct xxx
         typedef struct student
         {
              int num;
              int score;
         }STU,*stu;
    
         int main()
         {
              STU a;  //等同于struct student a
              stu b;  //等同于struct student *b;
         }
    
    • 结构体数组排序做法
      • 个人觉得本质上和数组的排序基本一样,唯一要注意的点就是一个结构体赋值给另一个结构体时,所有的结构成员都会被替换
         typedef struct student
         {
    	  char name[10];
    	  int score;
         }STU;
    
         int main()
         {
    	  STU p[10], temp;
    	  int i, j;
    
    	  for (i = 0; i < 10; i++)
    		  scanf("%s %d", p[i].name, &p[i].score);   //给结构体数组赋值
    
    	  for (i = 0; i < 10; i++)
    		  for (j = i + 1; j < 10; j++)
    			  if (p[i].score < p[j].score)
    			  {
    				  temp = p[i]; p[i] = p[j]; p[j] = temp;   //和数组的选择排序类似,只是交换的是一整个结构体里的内容而已
    			  }
         }
    
    • 结构体指针怎么用
      • 这个我认为也和普通的指针类似,只是类型从int/char这样子的变成了一整个结构体
      • 要注意取结构体指针的结构成员的写法,一般有下面的两种写法
         STU *p;
         (*p).score = 100;  //括号不能省略
         p->score = 100;   //更常见的写法
    
         p.score = 100;    //这是错的!!!!
    
    • 与普通指针类似,假设有STU *p=a(a是结构体数组),那么p即指向a[0] 的首地址,p+1指向a[1]的首地址

    • 共用体、枚举类型做法

      • 共用体和结构体长得很相似,但实际上它们大有不同:结构体的各个成员会占用不同的内存,互相之间没有影响;而共用体的所有成员占用同一段内存,修改一个成员会影响其余所有成员。
      • 一般运用较少,在前几个结构成员相同,但因为几个结构成员不同导致后面要保存不同类型的内容时会用到。
      • 枚举类型个人认为与宏定义较类似,直接举例说明
         enum week{ Mon, Tues, Wed, Thurs, Fri, Sat, Sun };    //定义方式为enum 枚举类型名{每个值对应的名字};  分号!!
    
         /*这样子没有给初值定义完,默认Mon代表0,Tues代表1,以此类推*/
    
         enum week{ Mon = 1, Tues, Wed, Thurs, Fri, Sat, Sun };
       
         /*给了第一个名字初值,那么后面的Tues则为2,Wed为3,每个加1*/
    
         enum week day;    //枚举类型,输出星期的例子
         scanf("%d", &day);
         switch(day)
         {
            case Mon: puts("Monday"); break;
            case Tues: puts("Tuesday"); break;
            case Wed: puts("Wednesday"); break;
            case Thurs: puts("Thursday"); break;
            case Fri: puts("Friday"); break;
            case Sat: puts("Saturday"); break;
            case Sun: puts("Sunday"); break;
            default: puts("Error!");
         }
    
    • 文件读写,文件中数据如何读进结构体数组
      • 首先需要定义一个FILE类型的指针变量
      • 打开文件的方式为 文件指针名=fopen("文件名","文件操作方式")
        • 常用操作方式分为 r:只读;w:只写;a:追加在末尾;b:二进制文件;+:读和写
        • 其中,用"r"打开文件时,文件必须存在用"w"打开文件时,若打开的文件不存在,则以指定的文件名建立该文件,若打开的文件已经存在,则将该文件删去,重建一个新文件
        • 文件可能会打开失败,这样fopen函数会返回NULL,在写程序时要注意对这部分有判断
        • 有打开就要有关闭,记得要用 fclose(文件指针名) 来关闭文件
      • 对文件数据进行读取、写入
        • fgets(用于储存的目标字符串,读取长度,文件指针名) 可以用于实现从目标文件中读取字符串,用法与注意事项以前使用fgets基本相同(会读入 等),唯一不同的是最后一个从stdin(从键盘读入)改为了文件指针(从文件读入)
        • fputs(想要存入的字符串,文件指针名) 可以实现将字符串写入目标文件中
        • fputc、fgetc 用法与上面两个基本相同,从目标文件读入一个字符
        • fscanf、fprintf 与scanf、printf基本相同,按格式从文件中读取、写入数据
        • feof(文件指针名) 用于判断文件是否已经读到末尾
         /*文件中数据读入结构体数组简单样例*/
         typedef struct student
         {
    	  char name[10];
    	  int score;
         }STU;        
         
         int main()
         {
              FILE* fp;
              STU p[50];
              int i = 0;
    
    	  fp = fopen("stu.txt", "r+");
    	  if (fp == NULL)    //记得要对打不开的情况做判断
    	  {
    		  exit(1);
    	  }
    
    	  while (!feof(fp))
    	  {
    		  fscanf(fp, "%s %d", p[i].name, &p[i].score);
    		  i++;
    	  }
    
    	  fclose(fp);    //读完记得关闭文件!!
         }
    
    • 其他一些对文件进行操作的函数
      • fseek(FILE *stream, long offset, int fromwhere):函数设置文件指针stream的位置。如果执行成功,stream将指向以fromwhere( 偏移起始位置:文件头为0(SEEK_SET),当前位置为1(SEEK_CUR),文件尾为2(SEEK_END) )为基准,偏移offset(指针偏移量)个字节的位置。如果执行失败(比如offset超过文件自身大小),则不改变stream指向的位置。
      • ftell(FILE *stream):用于得到文件位置指针当前位置相对于文件首的偏移字节数。
      • rewind(FILE *stream):将文件内部的位置指针重新指向开头。
      • remove(char *filename):用于删除指定文件。
      • rename(char *oldname, char *newname):将指定文件进行更名。

    1.2 本章学习体会

    • 上半学期C语言的学期也进入了尾声,文件部分感觉通过一次大作业已经掌握了很多
    • 而对于链表,我也试着将它运用到大作业里面,虽然写程序的时候遇到很多磕碰,但还是成功用链表写了程序的一部分
    • 虽然是老生常谈了,但还是想说要多实践多运用,可能在课堂上听着感觉链表挺简单的,但一上手写起来又是生不如死了(脑子:我学会了 手:不,你不会)
    • 没有任何捷径,多敲代码才是真理
    • 代码量(本次大作业):887行

    Q2.综合作业--“我爱成语”

    2.1 文件介绍

    2.1.1 头文件 idiom.h

    • 头文件中所有的结构体、函数声明以及函数的功能全部在截图上的注释中


    2.1.2 文件1 main.cpp

    • 主界面函数功能

      • 先在这里读取文件,检查文件是否存在
      • 导入到登录界面、游戏的主菜单
      • 根据用户不同的选择进入不同的函数
      • 普通的主函数,选择1/2开始玩游戏,3对题库进行管理,4/5展示游戏排行榜,6展示游戏说明,7切换用户(就是重新登录),0退出游戏
    • 代码截图


    2.1.3 文件2 menu.cpp

    • PrintStartMenu()函数

      • 程序刚开始时的主界面,用来引导用户注册、登录
      • 返回用户的选择,并传递给控制登录的函数
      • 在主界面如果选择切换用户会回到这里
      • 代码截图
    • LoginMenu()函数

      • 传入在上一个函数中的选择n,如果用户在上一个界面选择0则退出,选择1代表注册,2代表登录
      • 在这里打开储存用户信息的文件user.txt并读入结构体数组
      • 对于新用户注册,采用如下流程:用户输入用户名 --> 遍历结构体数组检查是否有重复用户名,如有重新输入 --> 用户输入密码 --> 创建成功,将该账户名和密码写入结构体数组,并写入到user文件的末尾 --> 用户使用这个id直接登录到游戏中
      • 对于用户登录,输入账号和密码,检查用户名和密码是否正确,如不正确提示“用户名或密码错误”并重新输入,如果正确则登录进游戏
      • 最后返回用户名,以便于排名更新等操作
      • 代码截图
    • PrintSelectMenu()函数

      • 菜单界面,传入用户的名字用于问候
      • 记录用户的选择返回主函数
      • 代码截图
    • PrintInformation()函数

      • 用于输出游戏说明
      • getch()和按任意键继续那句可以用system("pause")代替
      • 代码截图
    • ShowRanking()函数

      • 根据传入的数字展示游戏的排名
      • 先从文件中读入,再写到结构体数组中,然后再输出
      • 代码截图
    • InsertRanking()函数

      • 该函数传入三个数据:游戏号码(猜成语1,接龙2)、分数、用户名
      • 该函数的主要流程如下:读入排名文件并写入结构体数组中 --> 读取当前时间,和传入的三个参数一起储存到temp的临时的结构体中 --> 逐个比较直到全部排名读完或者出现一个排名的分数低于temp中的分数 --> 如果已有10个排名且读完了全部排名,则不对排名进行操作就返回;如果读完排名但排名小于10个则将新排名插在末尾;如果在末尾前读完则将比temp中分数低的全部后移一位 --> 建立新的临时排名文件,按新排好序的排名重新编写排名文件 --> 删除旧的排名文件,并将新的临时排名文件进行更名
      • struct tm是time.h中带有的结构体,里面的结构成员(年份、月份、日期、小时、分钟等等)都是int类型变量,gmtime()函数将当前时间储存到这个结构体中,经过一定处理后就是当前时间
      • 正因为它们都是int类型的变量,不能通过fputs函数写入到文件中,所以我又使用了sprint()函数,将数据转化成字符串类型,再进行存储,具体使用方法: sprintf(要存储到的字符串名,对数据进行格式化的类型,要处理的数据),其中第二项较为灵活,如%d就是转为十进制整数,%o就是整数转成八进制等等
      • 还用到了remove()和rename()函数,这两个在前面知识点总结部分有说明
      • 代码截图

    2.1.4 文件3 idiomedit.cpp

    • IdiomsEdit()函数

      • 该函数用于输出题库管理的菜单,并根据用户的选择进入不同的函数
      • 代码截图
    • GetIdiomsData()函数

      • 该函数读取成语文件的资料,并将它们储存到链表中
      • 使用了跨文件的全局变量COUNT记录成语数量
      • 使用的是带头节点的链表,要注意一开始给头节点指针空间并让head->next=NULL
      • 使用strchr()函数来寻找冒号,将这一点改成后,将前的作为成语本身存储,后面的为解释
      • 要注意要对找不到冒号的情况进行处理,如果文件中有一行没有冒号或者单独空行,VS会报错数组越界(好像是叫这个)
      • 代码截图
    • OutputIdioms()函数

      • 作用是输出所有成语
      • 注意,由于是带头结点的链表,所以临时指针要从头节点的next开始
      • 代码截图
    • InsertNewIdiom()函数

      • 用于让用户自己插入一个新的成语
      • 遍历判断这个成语是否有重复
      • 将新的成语插入到链表头节点,再往前移动头节点(这样比较方便)
      • 最后将新成语写入文件末尾,并及时fclose()进行保存
      • 代码截图
    • DeleteIdiom()函数

      • 用于让用户删除目标成语,其实就是链表中目标结点的删除
      • 和排名相同,创建了一个新的临时文件
      • 删除目标结点后遍历链表,将链表里的内容写入到新的临时文件中
      • 然后删除旧的文件,将临时文件重命名
      • 代码截图
    • SearchIdiom()函数

      • 用于搜索目标成语,输出这个成语的意思
      • 其实就是遍历链表进行对比而已
      • 代码截图

    2.1.5 文件4 game.cpp

    • GameMenu()函数

      • 游戏难度选择的菜单,使用了sleep()函数模拟倒计时的功能,其中sleep()函数中括号填的是毫秒数
      • 代码截图
    • GuessingGame()函数

      • 进行猜成语游戏执行的函数,根据难度不同会有不同的提示
      • 由于链表不能直接进行随机,所以我选择了根据成语数量生成一个随机数,然后让链表往下走这么次的方法来进行随机
      • USED数组是用来记录对应这个随机数的成语是否出现过了,其实可以并在链表的结构成员中
      • 而对于挖空,由于一个中文占两个字节,所以在随机挖空时随机数的范围除以2,最后挖空时再乘2比较好,防止挖空到一个字的尾加另一个字的头
      • 然后用户输入答案,检查是否超时,再与正确答案做比对,记录连续答对的题目数量
      • 代码截图
    • ChainGame()函数

      • 进行成语接龙游戏的函数
      • 由于数量比较大,就没有使用链表
      • 一开始是系统随机找题,然后让玩家进行回答
      • 如果回答正确,系统会在题库中随机找题一万次,比对第一个字和玩家输入的最后一个字(不采用遍历是为了增加随机性)
      • 如果回答错误,或是一万次没有找到合适的词语,就将重新随机出题
      • 用户回答的答案会在题库中进行比对,检查是否是成语、第一个字和系统出题的最后一个字是否相符、这个字有没有出现过
      • 代码截图

    2.2 运行结果

    2.2.1 登录界面

    2.2.2 游戏界面

    2.2.3 排名界面


    2.3 大作业总结

    2.3.1 碰到问题和解决方法

    Q1.莫名奇妙的报错?

    A1.这个似乎是函数定义、声明、使用时有一个函数名字写错了,检查哪个错了改掉即可

    Q2.还是莫名其妙的报错!

    A2.这个是我在GetIdiomsData()函数中提到的,文件中可能有一行没有冒号,或是最后多了一行空行,导致loc没有定位到冒号,此时他为NULL,在这个基础上再对字符数组进行操作必然越界。所以要注意对找不到冒号时的情况进行判断!

    Q3.链表无法随机,怎么随机出题
    A3.我选择了根据链表长度,生成随机数让遍历指针执行那么多次的next,从而实现随机

    Q4.挖空后成语变成乱码
    A4.这个就是挖空挖到一个字的尾和另一个字的头了,要注意一个汉字占两个字节,再进行操作,参考上面挖空部分的代码

    Q5.排名有更新后也出现乱码
    A5.因为按照“一个排名加一个换行”的方式输出,会导致最后一名后面多一个空行,所以我后来采用了“一个换行加一个排名”的方式,只要对第一行进行判断即可

    2.3.2 小结

    • 一开始看到这个作业其实是很懵的,完全不知道从何下手,但是实际写起来远比想象中的容易,所以首先面对这种大作业不应该产生畏难情绪
    • “哇要写这么多/效果这么炫酷,这是什么函数啊??”这种无意义的话就不必了,不要看到一个没见过的函数/没见过的效果就哇好难哇好难,有的时候它们真的很简单,实现的代码你也都学过,只是你有没有往那个方面想,别人想到了,自己没有想到而已
    • 遇到想要实现的功能又不知道何从下手,建议直接百度,说真的。我的sprintf()就是从百度学到的。不要说我不会就直接放弃了,还是要有点追求的嘛
    • 本次作业由于时间比较紧迫,很多小细节没有实现到位(例如加载、密码输入变成*号等功能),但这些小功能也都是从同学的程序中学到的一个思路,还是要加强优秀代码的阅读
    • 是一次综合能力全部运用的大作业,自我感觉还算满意吧
  • 相关阅读:
    零侵入性:循环重试功能
    Service有多个实现类,它怎么知道该注入哪个ServiceImpl类
    消息队列经典
    MyBatisplus
    图片存储服务器上
    Spring Boot 保护敏感配置的 4 种方法
    微服务组件
    Optional 实例
    date s 更改时间后不生效
    java运行时 报错 Error: Could not find or load main class test
  • 原文地址:https://www.cnblogs.com/silverash/p/12046078.html
Copyright © 2020-2023  润新知