HNU个人项目评测—中小学数学试卷自动生成程序

发布时间 2023-09-19 21:59:43作者: 1+1不等于2

 

一、前言  

  这次个人项目我和我的同伴zhs选择的都是C++,因此我们之间不存在诸如“没学过对面的代码根本不会读啊啊啊啊啊”这种问题,所以相对来说会很放心。这次的个人编程项目是让我们做一个中小学数学卷子自动生成程序,从题目要求的功能来看主要的难点在于如何去随机的添加符号以及确保符号添加的位置是正确的(不能出现诸如15+53-=这种奇怪的式子),除此之外的另一个难点就是要求我们在google的代码风格下进行编程,稍微有点了解的人都知道google对于C++的要求有多高……很多时候本来很好解决的东西在google的要求里瞬间就用不了了,你还得绕路去解决,所以我认为这也是一大难点。本博客旨在通过分析结队编程同伴的代码,从而达到避免未来自己编写C++程序的时候遇到同样的问题,同时也为后面的结对编程项目打下一定的基础。

二、正文

2.1 黑盒测试(exe测试)

  1.登录与退出功能:

  分析:很遗憾,在登录功能就出现了问题,前置空格和中间的多个空格以及后置空格都会出现无法登陆的bug,这一点稍后我们会在白盒测试中详细分析原因,虽然出现了bug但是整个的界面提示还是做的很不错的,这一点值得称赞。同时他这里每更换了一次界面之后都会进行一次刷屏,可以让整个程序的界面更加简洁,减少了复杂程度,值得称赞(就是截图的时候真心不好截)。

  分析:在这里退出功能很好的完成了他应有的功能功能。

 

  2.切换功能

  分析:这里经过测试没有任何的bug,切换功能正常运行,同时在输入错误信息时候也能正常提示错误信息,功能正常,当输入诸如切换为xx和切换为高这样的误导性词汇时也能正确进行判断,有效避免了误输入的情况,这是值得称赞的一点。

  3.出题功能

  最重量级的环节!可以说整个个人项目的核心就是看出题部分和写入部分能不能正常工作!

下图为生成的初中题目(15道)

下图为生成的高中题目(15道)

   分析:很明显,各项功能均正常!无论是输入越界的题目数量还是输入奇怪的数字都能正常显示错误信息,当输入范围内的数据时,就会进入题目显示界面,这一点是我的程序中并没有的,它可以很好的提供了已生成题目的预览而不必要再专门去看txt文件,而文件的写入功能虽然运行得十分正常,但是我还是发现了一点点没写在“甲方”要求里的问题:如果我提前删掉账号所属的文件夹,它并不会自动创建一个文件夹,也不会生成文件,不过考虑到要求中也并没有明确要求一定要生成文件夹,也就无可厚非啦♪(^∇^*)同时对于题目的重复部分我观察了10个txt,并没有出现重复的现象,因此可以大致判定为查重功能是存在且生效了的。

2.2 白盒测试(看源代码说话)

  引言:终于到了代码检测环节!前面的黑盒测试是基于用户的角度进行的测试,因为在实际的软件开发中用户一般只负责使用我们编写的程序,因此对于黑盒测试最重要的一项指标就是各项功能能不能如预期般正常工作,如果能就说明黑盒测试是成功的,也说明至少从用户的角度来说是没有大问题的。好了,废话不多说,让我们正式进入代码分析环节吧!

1.项目整体结构

   呃…可以看到整个项目就两个类,其中user类里面就是存储着用户的信息,还包括了一些获取与设置内部protected数据的方法,如下图:

   其他的就是各种函数以及全局变量了,但是从后期的角度来说这种编程方式不利于后期增加功能以及对项目进行维护,因此不是很推荐这样子将所有函数写在一起的方式,更推荐java那种面向对象编程的思想。下面我们会对各个函数进行详细的展开:

2.具体函数

(1)fileread

std::vector<User> fileread()  // 将zhanghaoku.txt文件读入
{
  User temp;
  std::vector<User> b;
  std::ifstream ifs;
  ifs.open(R"(zhanghaoku.txt)", std::ios::in);
  if (!ifs.is_open()) {
    std::cout << "打开文件失败!";
    exit(0);
  }
  std::string shuju;
  while (getline(ifs, shuju)) {
    int weizhi1 = shuju.find_first_of('\t');
    int weizhi2 = shuju.find('\t', weizhi1 + 1);
    temp.setname(shuju.substr(0, weizhi1));
    temp.setkey(shuju.substr(weizhi1 + 1, weizhi2 - weizhi1 - 1));
    temp.settype(shuju.substr(weizhi2 + 1, 1));
    b.push_back(temp);
  }
  return b;
}

  分析:可以从这个函数名看出来这个函数的功能就是对外部的账号库进行打开并读取,这里使用了ifstream的函数进行文件的打开,并且一行行读取进入程序之后将其添加到std::vector中并返回,这种善用stl容器的方法我是十分推荐的,同时他是以制表符作为分隔符进行判断,读取外部账号库这一点是要求中没有的,我认为是属于锦上添花,因为这样做可以方便后期添加账号进入到账号库中,但是有一点小小小小小问题:没考虑到加密(虽然这么说好像有点吹毛求疵哈哈哈),这样在使用的时候外人可以很轻松的篡改这个账号库,因此我认为这是一点点需要改进的地方。

(2)GetRand函数

  就是一个获取随机数的函数,没啥好说的。

(3)login函数

 

void login(std::vector<User> &b)  // 账号登录
{
  int temp = 0;
  int index = 0;
  std::string str;
  std::string account;
  std::string password;
  while (true) {
    std::cout << "请输入正确的用户名、密码,中间由空格隔开" << std::endl;

    getline(std::cin, str);
    index = str.find(" ");
    account = str.substr(0, index);
    password = str.substr(index + 1, str.length() - index - 1);
    // 用于判断
    for (long i = 0; i < b.size(); i++) {
      if (account == b[i].getname() && password == b[i].getkey()) {
        std::cout << "成功登录!" << std::endl;
        temp = 1;
        jilu = i;
        logginguser = account;
        break;
      }
    }
    if (temp == 0) {
      std::cout << "输入错误" << std::endl;

    } else
      break;
  }
}

  分析:这就是登录函数了,还记得之前我们提到的bug吗?现在一看到这个代码我瞬间明白为什么会有之前那些bug了,因为他匹配的就是空格!也就是说如果我输入了多个空格,他会把第一个空格当做index,然后后面的全部被截取为密码了!在这个情况下肯定是不可能和账号库中的内容进行匹配了。要修改这个问题也很简单,在后续的密码中进行空格处理即可。并且还存在一个问题:那个logginguser只使用了两次:一次是在全局变量中进行了定义,剩下的一次就是在这里被赋值,除此之外没有其他任何的内容,因此是可以被删除的。

  虽然说这个存在bug,但是他对于输入的账号密码的处理是我需要进行学习的。因为它使用了getline()函数获取整行的输入,这样做可以避免很多输入的bug出现,我当初就是卡在这里很久。

(4)write函数

void write(std::string account, std::string text) {  // 将题目写入文件
  char FileName[100];
  struct tm *ptr;
  time_t It = time(NULL);
  ptr = localtime(&It);
  strftime(FileName, 30, "%y-%m-%d-%H-%M-%S.txt",
           ptr);  // 以年月日时分秒对输出文件命名
  std::string checkPath = account + "/1.txt";
  std::ifstream checkFile(checkPath.c_str());
  std::string line;
  checkPath = account + "/" + FileName;
  std::ofstream outfile(checkPath.c_str(), std::ios::app);
  outfile << text << "\n";
  outfile << std::endl;
  return;
}

  分析:也没啥好说的,使用了windows自带的time_t结构体获取时间,并且通过strftime进行时间格式的转换(因为获取的时间是一个从1970.1.1开始计算到现在经过的秒数,unsigned int类型),进而达到题目要求的输出文件格式,最后进行文件的输出并且换行。在这里它是将题目一行行输出的,同时之前提到的那个小问题:文件夹不存在时无法正常写入,这里很明显没有文件夹是否存在进行判断,因此自然也不会报错了,从要求的角度来说这无可厚非,但我还是建议能增加判断以完善其功能最好。

(5)Check函数

bool check(std::string account, std::string text) {  // 检查是否重复
  std::string checkPath = account + "/Check.txt";
  std::ifstream checkFile(checkPath.c_str());
  std::string line;
  while (std::getline(checkFile, line)) {
    if (line == text) {
      return false;
    }
  }

  if (line == text) {
    return false;
  }
  return true;
}

  分析:乍一看起来这个检查路径好像是绝对路径?还有后面那个Check.txt是什么东西?这个函数的整体功能我能理解,就是将对应路径的文件读入,但是用的路径名好像有点问题……不确定,后续再看看具体使用吧

(6)Jia函数(……改为CreateSymbol不是更好吗QAQ)

std::string jia(int n, std::string text) {  // 符号位置随机生成符号
  if (n != 0) {
    int opt = getRand(0, 3);
    switch (opt) {
      case 0:
        text += "+";
        break;
      case 1:
        text += "-";
        break;
      case 2:
        text += "*";
        break;
      case 3:
        text += "/";
        break;
    }
  } else
    text += "=";
  return text;
}

  分析:就是生成随机数,然后添加符号(小学)的一个函数,可以从这个函数看出他是有面向对象的思想的(将题目拆成很多个部分,比如操作数,操作符,挨个添加),这一点值得表扬,因为面向对象编程可以让我们后续对其修改和维护的时候能更好的进行处理。

(7)fuhao(………………改为CreateJunior&Senior不好吗QAQ)

std::string fuhao(int OpNumber, int bracket_left,
                  int bracket_num,  // 对于初中高中特殊符号的添加
                  std::string type, int n, std::string text) {
  bool falg = false;
  int sign = getRand(0, 2);
  for (; n > 0;) {
    n--;
    bool du = false;
    if (n == OpNumber - bracket_left) text += "(";
    if (n == 1 && falg == false) {
      sign = getRand(1, 2);
    }
    if (sign == 1 && type == "2") {
      falg = true;
      text += "√ ̄";
    }
    if (sign == 1 && type == "3") {
      falg = true;
      text += "sin";
      du = true;
    }
    if (sign == 2 && type == "3") {
      falg = true;
      text += "cos";
      du = true;
    }
    int x = getRand(1, 100);
    text += std::to_string(x);
    if (sign == 2 && type == "2") {
      falg = true;
      text += "^2";
    }
    if (du == true && type == "3") {
      falg = true;
      text += "°";
    }
    if (n == OpNumber - bracket_left - bracket_num + 1) text += ")";
    text = jia(n, text);
  }
  return text;
}

  分析:可以看到他将两个功能合一:添加初中符号和高中符号,通过一个sign进行判断是具体要添加哪种类型的符号,而type这一变量是用来判断具体添加哪个符号的,这一点很不错,至少能做到让人一眼看出来这是要干嘛的,只是感觉唯一的缺点是if语句有点太多了,能改进一下自然最好,同时还有一个小问题:falg……是个啥TAT,希望是打错了而不是真心这么命名的!

(8)Xiao函数(……没看懂这命名)

void xiao(std::string account, std::string text,
          std::string type) {  // 对题目操作数和括号位置的随机生成
  labelss:
  int OpNumber = getRand(2, 5);
  int n = OpNumber;
  int bracket_left = 0;
  int bracket_num = 0;
  if (OpNumber > 2 && getRand(0, 1) == 1) {
    if (OpNumber == 3) {
      bracket_left = getRand(1, 2);
      bracket_num = 2;
    } else if (OpNumber == 4) {
      bracket_left = getRand(1, 3);
      if (bracket_left == 3)
        bracket_num = 2;
      else
        bracket_num = getRand(2, 3);
    } else {
      bracket_left = getRand(1, 4);
      if (bracket_left == 4)
        bracket_num = 2;
      else if (bracket_left == 3)
        bracket_num = getRand(2, 3);
      else
        bracket_num = getRand(2, 4);
    }
  }
  text = fuhao(OpNumber, bracket_left, bracket_num, type, n, text);
  if (check(account, text) == false)
    ;
  {
    // goto labelss;
  }
  std::cout << text << std::endl;
  write(account, text);
}

  分析:这里就是对题目操作数和括号位置的随机生成,同理,if语句有点多了……理逻辑的时候会有点复杂,但是对于括号的处理还是十分不错的,值得学习。与此同时就有一个严重的问题了:这个check函数功能不明确,为false之后啥也没干,这点需要改进!

(9)Table函数

void table(std::vector<User> &b) {  // 登录后界面
  if (b[jilu].gettype() == "1")
    std::cout
        << "准备生成小学数学题目,请输入生成题目数量(输入-1将退出当前用户,重"
           "新登录):"
        << std::endl;
  if (b[jilu].gettype() == "2")
    std::cout
        << "准备生成初中数学题目,请输入生成题目数量(输入-1将退出当前用户,重"
           "新登录):"
        << std::endl;
  if (b[jilu].gettype() == "3")
    std::cout
        << "准备生成高中数学题目,请输入生成题目数量(输入-1将退出当前用户,重"
           "新登录):"
        << std::endl;
  std::cout << "输入“切换为XX”可切换出题模式" << std::endl;

  分析:和我的做法一样,这里就是通过判断当前用户的属性进行不同界面的输出,使用了std::vector进行用户数据的存储,同时在前面登录的时候存储了对应的jilu变量,达到对后续进行进一步判断的结果,这点很值得学习,善用stl容器以及全局变量对整个软件的开发是很有帮助的,效率也会有所提升。

(10)make函数

void make(std::vector<User> &b) {  // 登录后是否转换模式以及生成题目
  labelss:
  table(b);
  std::string n;
  int num;
  std::cin >> n;
  system("cls");
  if (n == "-1") {
    return;
  }
  std::string q3 = n.substr(0, 6);
  if (q3 == "切换为") {
    if (n == "切换为小学") {
      b[jilu].settype("1");
    } else if (n == "切换为初中") {
      b[jilu].settype("2");
    } else if (n == "切换为高中") {
      b[jilu].settype("3");
    } else
      std::cout << "请输入小学、初中和高中三个选项中的一个" << std::endl;
    goto labelss;
  }

  else {
    sscanf(n.c_str(), "%d", &num);
  }
  if (num < 10 || num > 30) {
    std::cout << "有效输入范围是“10-30”,请重新输入" << std::endl;
    goto labelss;
  }

  for (int i = 0; i < num; i++) {
    std::string text = std::to_string(i + 1) + ". ";
    { xiao(b[jilu].getname(), text, b[jilu].gettype()); }
    std::cout << std::endl;
  }
  system("pause");
  system("cls");
  goto labelss;
}void make(std::vector<User> &b) {  // 登录后是否转换模式以及生成题目
  labelss:
  table(b);
  std::string n;
  int num;
  std::cin >> n;
  system("cls");
  if (n == "-1") {
    return;
  }
  std::string q3 = n.substr(0, 6);
  if (q3 == "切换为") {
    if (n == "切换为小学") {
      b[jilu].settype("1");
    } else if (n == "切换为初中") {
      b[jilu].settype("2");
    } else if (n == "切换为高中") {
      b[jilu].settype("3");
    } else
      std::cout << "请输入小学、初中和高中三个选项中的一个" << std::endl;
    goto labelss;
  }

  else {
    sscanf(n.c_str(), "%d", &num);
  }
  if (num < 10 || num > 30) {
    std::cout << "有效输入范围是“10-30”,请重新输入" << std::endl;
    goto labelss;
  }

  for (int i = 0; i < num; i++) {
    std::string text = std::to_string(i + 1) + ". ";
    { xiao(b[jilu].getname(), text, b[jilu].gettype()); }
    std::cout << std::endl;
  }
  system("pause");
  system("cls");
  goto labelss;
}

  分析:这里它采用了substr函数对输入的数据进行处理,如果截取的部分是“切换为”(这里截取长度为6的原因是UTF-8编码里存储一个中文字符长度为3),就进行下一步判断,如果不是小学初中和高中三个中的一个就报错,用goto语句回到开始。这一点需要提出批评,因为goto语句很容易造成问题,这也是googlestyle不允许的。虽然有点小问题,但是整体判断思路是正确的,也不存在额外的问题,后续如果判断输入的数据不为“切换为xx”,就会进行进一步输入,如果输入的不是数字以及数字超出范围也会使用goto语句回到开头,如果判断是正常就进入下一步的Xiao函数(这里的大括号我也没看明白是干嘛的,感觉也是多余,建议改正),从Xiao函数返回后就会调用系统指令暂停并且清屏,等待下一步判断。

(11)mainmenu函数

void makemainmenu()  // 主菜单
{
  printf("---------------------------------------------\n");
  printf("\t\t中小学数学卷子自动生成程序\n");
  std::cout << std::endl;
  printf("\t\t\t1.登录\n");
  std::cout << std::endl;
  printf("\t\t\t2.退出\n");
  printf("---------------------------------------------\n");
};

  分析:主界面,十分简洁,输出了一些信息。

(12)Keydown函数

void Sys::keyDown()  // 交互处理
{
  labelss:
  makemainmenu();
  printf("请输入您要的功能\n");
  int userkey = 1;
  std::cin >> userkey;
  std::cin.get();
  system("cls");
  switch (userkey) {
    case 1: {
      std::vector<User> b;
      b = fileread();
      login(b);
      system("cls");
      make(b);
      goto labelss;
    }

    case 2:
      printf("感谢您的使用!退出成功!\n");
      system("pause");
      exit(0);  // 关闭程序
    default:
      printf("您的输入有误!请检查!\n");
      goto labelss;
  }
}

  分析:可以看到这就是交互判断函数,这里值得表扬与学习,将显示信息与具体的交互判断分离可以让人更清晰的看出交互函数的作用,同时也更有利于集中代码逻辑的判断,专注于代码的debug。

(13)main函数

int main() {
  srand((int)time(NULL));
  Sys a;
  a.keyDown();

  return 0;
}

  分析:这里通过keydown函数进入交互界面,十分简洁,很符合要求。

2.3 规范化检查

白盒测试之后就是要求中的满足google代码规范,这里我不详细展开,只提几个我认为很重要的点并且每个点只举一个例子:

  1.变量命名奇怪:

  如果没有注释谁知道这个函数是干嘛的……这种命名让人摸不找头脑啊[○・`Д´・ ○]类似的命名还有很多呢!

  2.括号位置不对:

   大括号大部分时候都应该在前一行的末尾,这里出现了错误。

  3:引用变量的时候没加const:

   引用变量前要加const(除非是std::default_random_engine这种),否则就要用指针,这一点是为了变量的安全性。

 类似的问题还有很多,在检查的同时我也意识到了自己的代码有一部分也是不满足要求的,因此后续我们需要共同对其进行修改以达到更好的规范!

三、结语以及感想

同伴项目的优点:

  1.使用了容器,让项目的性能增加

  2.有面向对象编程的思想,即使实施的时候不是那么完善

  3.大体上是符合google代码规范的

  4.题目合理性十足,小学题目不会出现诸如1=这样奇怪的题目,同时初中高中的题目也都满足要求

  5.通过外置账号库,达到后续能实时添加的目的

  6.生成的题目随机性很强,10个txt文件中都没有任何一个重复

缺点:

  1.类的数量太少了而且很敷衍,很明显就是为了题目要求而产生的类,并不是真正的面向对象编程

  2.外置账号库没有加密,篡改十分容易

  3.函数命名奇怪,如果没有注释根本看不懂函数的功能是什么

  4.查重功能有点奇怪

  5.部分方法体的if语句似乎使用过多了

  6.登录存在bug

结语:

  总的来说同伴完成的还是十分不错的,虽然登录部分有一些bug,但是总体还是在可接受范围之内,且后续的完成度也很高,达到了预期的水准。通过对他代码的分析,我不仅发现了它的代码存在的问题,同时也意识到了自己的代码也存在类似的错误与bug,在后续的结对编程中虽然我们不再使用C++语言,但是也是以此为参考进行修改,因此在结对编程中我会对其进行优化,争取做到将两者的优点进行结合,以达到更高的代码质量以更好的完成目标!