浙工大c++课设讲解

<0x00> 前言

我的题目是幼儿园的信息管理系统(第29号)
运行在Cmake 3.25.3+mingw 12.2.0

基本要求:
能从文件读出一个班级小朋友的信息,并能将管理信息保存到文件
小朋友的信息包括:姓名、出生年月(年龄)、性别、住址、身高、体重、父母联系人姓名、联系电话,备注信息等

功能要求:
1、系统以菜单方式工作;
2、增加修改和删除小朋友信息;
3、查询某个小朋友的父母联系人姓名及联系电话;查询小朋友住址;
4、显示所有小朋友信息:能根据小朋友的身高、体重分别排序,或者按身高体重比排序;
5、能按性别统计班级男女生人数比例;
6、可以进行小朋友信息的文件读写。

实现上面功能是挺简单,所以我加入了一些新功能
于是就实现了些杂七杂八的东西(之后也会重点介绍的):

  • 双向模板链表
  • 方向键界面控制
  • csv的文件读取和导出
  • 多文件管理
  • 从临时文件恢复

本来想实现更多的,实际上摸了就没写😋
(主要是真没啥好写的)

你可以在本站gitea下载源代码
(课设报告就不发了,自己写写挺快的)

<0x01> 双向链表实现

//Mylist.cpp 仅列出声明,完整代码见源文件
#pragma once
#include <cstddef>  //NULL的定义在这个头文件里(我用的是mingw)
using namespace std;
template <class T>
class MyList;   //提前声明类型
template <class T>
class MyNode
{
    T content;       // 存放内容
    MyNode<T> *next; // 指向下一个Node
    MyNode<T> *back; // 指向前一个Node
    friend class MyList<T>; // 为List类提供访问权限(方便)
    friend class Tool;  // 为Tool类提供访问权限(方便)
    MyNode();

public:
    MyNode(const T &content, MyNode *next = NULL, MyNode *back = NULL);
    MyNode(const MyNode &n);    //一些节点的构造函数
    MyNode<T> &operator=(const MyNode &n);  //保险起见,重载下赋值运算
};

template <class T>
class MyList
{
    int length;                       // List的长度
    MyNode<T> *head;                  // 指向头节点
    MyNode<T> *end;                   // 指向尾节点
    MyNode<T> *Find(int index) const; // 内部按index搜索用
    friend class Tool;                // 为Tool类提供访问权限(懒得写访问的函数了)

public:
    MyList();
    bool Insert(const T &c, int n = 0); // 插入List,可指定插入位置
    bool Delete(int index);             // 按index删除List元素
    void Clear();                       // 清空List内容
    int GetLength() const;              // 返回List的长度
    MyList<T> &operator=(const MyList<T> &n);   //保险起见,重载下赋值运算
    T *operator[](int index) const;     // 外部按index搜索用
    ~MyList();                          // 析构是门学问
};

泛型类 MyNode<T>

主要就是存放内容物的节点,为了数据结构的可拓展性,就写了个泛型
基本就没啥好说的,关键就是用指针关系吧各个节点连接起来

我这里采用的是双向链表的数据结构,即每个节点可以找到它的前一个后一个
所以我这里的MyNode类需要两个指针,一个指前一个节点,一个指后一个节点

泛型类 MyList<T>

说白了,这部分的内容就是把各个节点拼起来
为了首尾插入效率与保险起见,这个双向列表带有空的首尾节点
(但最终代码没有完成对末尾插入的优化,忘了

总之,数据结构图如下
数据结构
每个节点都可以找到它的前后节点
虽然对插入操作没什么优势,但可以加速删除操作
对于删除操作,仅需传入节点位置即可删除
相当于把要删除的东西抽出来,再两边节点建立连接
(单向链表的删除比较抽象)

对于Clear()操作,相当于走一遍链表,边走边删对象
走完了再回归到初始状态
析构就是Clear()后再把首尾节点也删了

剩下的都挺直观的,也不说了

<0x02> 方向键控制

方向键控制的代码写在UI.hpp
UI.hpp里面都是一堆静态函数,提供一些标准化输出实现
(压缩代码量)

//能实现方向控制的示例代码
#include <conio.h>  //必要的头文件
using namespace std;
int main()
{
    char c;
    c = _getch();
    swich(c)
    {
    case 72:
        //上方向键的代码
    case 80:
        //下方向键的代码
    case 75:
        //左方向键的代码
    case 77:
        //右方向建的代码
    case 13:
        //回车键的代码
    }
    return 0;
}

关键就是_getch()这个函数,他会捕获命令行界面的按键,然后返回对应按键的编码
其他的按键编码网上应该都能找到的

顺带讲下UI.hpp里的东西

结构体 UIInfo

就是用来传递菜单信息的结构体
好让主函数知道进行了什么操作,选了哪一个

UI类

里面都是静态函数

Confirm(...)函数用来显示确认菜单
yesText写确认的文字
noText写取消的文字
content写显示的内容

Select(...)函数显示选择菜单
有三个重载
总之可以显示标题,功能选择项,一般选择项,描述词,默认指针位置
title标题
function功能选择项
content一般选择项
head描述词
index默认指针位置
一般选择项十个每页,功能选择项始终显示

Show(...)函数就是个简单的输出函数
没啥好说的,让输出好看点而已

<0x03> CSV文件的读写

CSV文件的读写代码写在ManageTool.hpp

//导出为CSV文件
//原代码的逻辑已经嵌在相关函数里了,这里单独拿出来
#include <fstream>
#include <cstddef>
#include <string>
#include "MyClass.hpp"
#include "MyDate.hpp"
#include "UI.hpp"
using namespace std;
string temp;    //当前文件路径
MyClass *selectClass;    //MyClass是我写的班级类
ofstream writeFile;     //写入文件流
void ExportCSVFile()
{
    writeFile.open(temp.substr(0, temp.length() - 4) + "-" + selectClass->className + ".csv", ios::out);
    writeFile << "Class name: " << selectClass->className << "," << selectClass->children.GetLength() << ",\n";
    writeFile << "Name,Birthday,Age,Gender,Address,Height,Weight,Parent's name,Parent's phone,note,\n";
    for (int i = 0; i < selectClass->children.GetLength(); i++)
    {
        writeFile << selectClass->children[i]->name << ",";
        writeFile << selectClass->children[i]->birthday.GetDate() << ",";
        writeFile << selectClass->children[i]->age << ",";
        if (selectClass->children[i]->isBoy)
        {
            writeFile << "Boy,";
        }
        else
        {
            writeFile << "Girl,";
        }
        writeFile << selectClass->children[i]->address << ",";
        writeFile << selectClass->children[i]->height << ",";
        writeFile << selectClass->children[i]->weight << ",";
        writeFile << selectClass->children[i]->parentName << ",";
        writeFile << selectClass->children[i]->parentPhone << ",";
        writeFile << selectClass->children[i]->note << ",";
        writeFile << "\n";
    }
    writeFile.close();
    UI::Show("CSV file has been generate");
    break;
}
//由CSV文件导入
//原代码的逻辑已经嵌在相关函数里了,这里单独拿出来
#include <fstream>
#include <cstddef>
#include <string>
#include <vector>
#include "MyClass.hpp"
#include "MyDate.hpp"
#include "UI.hpp"
using namespace std;
string path;    //路径
MyClass *selectClass;    //MyClass是我写的班级类
ifstream readFile;      // 读取文件流
Student Create(vector<string> &list)
{
    //由传入的list来创建Student对象并返回
    //代码略,见原代码
}
void ImportCSV()
{
    readFile.open(path + "\\" + files[u.index], ios::in);
    selectClass->children.Clear();
    string temp;
    getline(readFile, temp, ',');
    getline(readFile, temp, ',');
    readFile.get();
    vector<string> list;
    int num = atoi(temp.c_str());
    for (int i = 0; i < 10; i++)
    {
        getline(readFile, temp, ',');
    }   //用来忽略表头的
    for (int i = 0; i < num; i++)
    {
        list.clear();
        readFile.get(); //吞上一行回车
        for (int j = 0; j < 10; j++)
        {
            getline(readFile, temp, ',');
            list.push_back(p.assign(temp));
        }
        selectClass->children.Insert(Create(list), selectClass->children.GetLength());
    }
    readFile.close();
}

CSV文件其实就是数值1,数值2,...这样组成的文件,这种文件格式可以被Excel读取
输出的时候先输出基本班级信息,然后按行输出每个人的信息
导入CSV时先读取基本班级信息,然后按行导入学生信息

输出一般不大会遇到问题,就是读取时文件指针的控制是个问题
这里在读取时用getline(ifstream,string,char)来读取,碰到,就截断
前面还有readFile.get()来吞上一行的空格

<0x04> 多文件管理

为了方便备份和多幼儿园管理(?),实现了多工程文件的选择

选择和访问都应该不成问题,但获取对应文件夹下指定类型的所有文件是个问题

//关键代码
#include <io.h>
#include <vector>
#include <string>
using namespace std;
void GetFiles(string path, vector<string> &files, string fileType) // 获取指定路径下指类型的所有文件名
{
    intptr_t hFile = 0;          // 句柄编号
    struct _finddata_t fileInfo; // 文件信息结构体
    if ((hFile = _findfirst((path + "\\*" + fileType).c_str(), &fileInfo)) != -1)
    {
        do
        {
            files.push_back(fileInfo.name); // 给files加入文件名
        } while (_findnext(hFile, &fileInfo) == 0);
        _findclose(hFile); // 关闭句柄
    }
}

这段代码实现了获取指定路径下指定文件类型的所有文件名
通过句柄实现(也就是用系统api)
最后的结果会保存在传入的files

注意,网上的多数代码会将句柄编号的类型写为long
但在win10环境下,使用long类型的句柄会导致精度丢失进而导致错
使用intptr_t就没这个问题

<0x05> 从临时文件恢复

为了防止用户系统崩溃(也可能程序崩溃)导致的数据丢失
管理系统在选择文件后会新建一个一样的同名的.tmp临时文件
后续的所有文件写入操作都会对这个.tmp文件操作
只有在用户正常退出系统时才会将.tmp文件保存为.dat文件

具体来说就是选择文件时并没有真正打开选择的文件,而是读取到文件名
在选择班级之后,正常读取文件
如果在班级管理界面里修改了相关数据,并不是修改原文件,而是创建同名临时文件,将修改后的结果写入
最后用户正常退出后,在主对象析构中把原文件删了,然后把临时文件的后缀改成.dat

代码就不单独拆出来了,原代码里有具体标明的

<0x06> 把所有东西拼起来

总体采用了UI与处理分离的设计
UI.hpp负责所有的界面输出,ManageTool.hpp负责所有的数据处理和页面逻辑

main()函数里就建立了Tool对象然后调用MainDisplay()启动
之后不同的功能就是不同的函数,不同的页面也是不同的函数
页面函数调用下一个页面的函数来实现功能的切换
页面函数相当于不同功能的调度器,页面输出还是靠UI.hpp的函数
UI的选择结果由UIInfo传递

其他的文件只是定义所需要的不同的类型

这样就吧所有的东西都拼起来了

后记

课设的基本要求都不难,但是都要完成
如果要拿优秀,肯定是要扩充功能的
如果想不到能扩充啥功能,可以优化UI
课设一般也不大需要多少时间,熟练的话写完课设大概3天