引言
这学期最后的数据结构课程设计需要我们完成一个简单的小程序,我选择了一个机票售卖系统,实现了一些基本的功能;因为时间给的比较短,又赶在复习周补课,所以并没有什么突出的地方,我就在这里聊聊我的代码实现和可以进一步改进的地方;
注:该程序并没有使用C++的面向对象部分内容 而是使用面向过程编程,主要使用了C++的一些容器和函数;
实现过程
基本功能
功能很简单,就是以下五种:
1,购票
2,退票
3,显示用户信息
4,查询用户信息
5,查看航班信息
这里因为是售票系统,所以对航班的增加删除等操作并没有添加,当然如果你想加也很容易实现;
虽然功能很简单,但是对于输入信息的判断需要保持严谨,所以如何制定判断规则也是要考虑的;
数据结构
首先考虑了用什么数据结构存储用户信息和航班信息,因为需要快速检索信息,并且在这里并没有使用数据库而使用最基本的文件操作,所以就考虑使用哈希表来存储用户信息和航班信息,并将key值分别设置为身份证后六位 和 航班号;(这里通过map容器来实现哈希表的操作)
结构体
然后就考虑用户和航班的结构体,代码如下:
// 客户信息结构体
struct Person {
string name; // 姓名
string age; // 年龄
string gender; // 性别
string idNum; // 身份证号
string myAirNum; // 用户航班号
};
// 航班信息结构体
struct Airplane {
string airNum; // 航班号
string startPoint; // 起点
string destination; // 目的地
string tickets; // 票数
string ticketPrice; // 票价
string departureTime; // 起飞时间
};
很简单,也没什么可以说的,可能有人会发现:为什么全部变量都用的是string类型?主要考虑到初始化时要把文件内容读取到内存中,而从文件种读取的都是字符串,所以就直接全部定义成了字符串;
当然这样不一定是最好的办法,我也想到了定义一套转换体系,但是时间比较紧所以就没有过多在这里纠结;如果你有更好的方法也欢迎交流一下!
文件
因为在这我并没有用到数据库而使用文件,所以并不需要考虑表结构的设计,但是还是要设计一下基本的文件格式的;
首先文件选用两个.txt文件,一个是userInfo存放用户信息,一个是airplane存放航班信息;
接下来需要考虑如何存放,因为初始化要把文件内容读入内存中,所以我定义每个用户信息(航班信息)占一行,每个属性间以一个空格划分,这样读取文件时一次就只读一行,读入后以空格为基础划分字符串;
文件内容如图:
函数
下面就可以想想需要什么函数了,我把函数分为两大类:基本功能函数 和 工具函数;
函数注释已经很详细了,除非特殊的我会详细说一下,其他就不多说了;
一、基本功能函数
函数声明:
void initInfo(); // 初始化信息
void UI(); // UI界面
void showUserInfo(); // 显示输出用户信息
void showAirplaneInfo(); // 显示输出航班信息
void sellTickets(); // 售票
void selectUserInfo(string idNum); // 查询用户信息
void refundTickets(string idNum); // 退票
UI界面
打印出来操作界面以及操作的汇总;
/// <summary>
/// UI界面
/// </summary>
void UI() {
while (true) {
// 打印界面
cout << "************欢迎使用xxxx航空购票系统**********" << endl;
cout << "************** 1, 购 票 **************" << endl;
cout << "************** 2, 退 票 **************" << endl;
cout << "************** 3,查看购票信息 **************" << endl;
cout << "************** 4,查找用户信息 **************" << endl;
cout << "************** 5,查看航班信息 **************" << endl;
cout << "************** 6, 退 出 **************" << endl;
int choose; // 选项
cin >> choose; // 输入选择
switch (choose) {
case 1 :
sellTickets(); // 售票
break;
case 2: {
string id;
while (true) {
cout << "请输入退票人的身份证号码:";
cin >> id;
// 判断输入身份证号是否合法
if (idNumIsLegal(id)) {
break;
}
cout << "身份证号不合法,请重新输入!" << endl;
}
refundTickets(id); // 退票
break;
}
case 3 :
showUserInfo(); // 查看购票信息
break;
case 4: {
string id;
while (true) {
cout << "请输入查询用户的身份证号码:";
cin >> id;
// 判断输入身份证号是否合法
if (idNumIsLegal(id)) {
break;
}
cout << "身份证号不合法,请重新输入!" << endl;
}
selectUserInfo(id); // 查看查询用户的信息
break;
}
case 5 :
showAirplaneInfo(); // 查看航班信息
break;
case 6 :
cout << "欢迎下次使用!" << endl;
exit(-1); // 退出
}
system("pause");
system("cls");
}
}
初始化信息
主要就是为了开始把文件中的内容初始化到内存中,方便之后的操作;
/// <summary>
/// 初始化信息
/// </summary>
void initInfo() {
ifstream ifs01;
// 对航班信息进行初始化
ifs01.open("airplane.txt", ios::in); // 打开文件
// 如果文件打开失败
if (!ifs01.is_open()) {
cout << "文件打开失败!\a" << endl;
exit(-1);
}
// 如果航班信息不为空
string ainfo; // 临时存放每一个航班信息
// 按行读取客户信息
while (getline(ifs01, ainfo)) {
vector<string> separateInfo; // 存放分离的信息
separateInfo = split(ainfo, " "); // 分离航班信息
Airplane airplane; // 存放临时航班信息
airplane.airNum = separateInfo[0]; // 存放航班号
airplane.startPoint = separateInfo[1]; // 存放起点
airplane.destination = separateInfo[2]; // 存放目的地
airplane.tickets = separateInfo[3]; // 存放票数
airplane.ticketPrice = separateInfo[4]; // 存放票价
airplane.departureTime = separateInfo[5]; // 存放起飞时间
// 航班信息存储
string key = separateInfo[0]; // 获取key值,即航班号
airplaneInfo.insert(pair<string, Airplane>(key, airplane)); // 将航班信息存入map
}
ifs01.close();
// 对用户信息进行初始化
ifstream ifs02;
ifs02.open("userInfo.txt", ios::in); // 打开文件
// 如果文件打开失败
if (!ifs02.is_open()) {
cout << "文件打开失败!\a" << endl;
exit(-1);
}
// 如果用户信息不为空
string info; // 临时存放每一个客户信息
// 按行读取客户信息
while (getline(ifs02, info)) {
vector<string> separateInfo; // 存放分离的信息
separateInfo = split(info, " "); // 分离客户信息
Person person; // 存放临时客户信息
person.name = separateInfo[0]; // 存放姓名
person.age = separateInfo[1]; // 存放年龄
person.gender = separateInfo[2]; // 存放性别
person.idNum = separateInfo[3]; // 存放身份证号
person.myAirNum = separateInfo[4]; // 存放航班号
// 客户信息存储
string key = separateInfo[3].substr(13); // 身份证后六位为map索引值key
userInfo.insert(pair<string, Person>(key, person)); // 将客户信息存入map
}
ifs02.close();
}
售票
增加用户并存入文件;
/// <summary>
/// 售票
/// </summary>
void sellTickets() {
// 1,录入用户信息
Person person; // 买票客户
// 输入姓名
while (true) {
cout << "请输入姓名:";
cin >> person.name;
// 判断姓名是否合法
if (nameIsLegal(person.name)) {
break;
}
cout << "姓名不合法,请重新输入!" << endl;
}
// 输入年龄
while (true) {
cout << "请输入年龄:";
cin >> person.age;
// 判断输入年龄是否符合常理
if (stringToInt(person.age) > 110 || stringToInt(person.age) < 0) {
cout << "年龄不合法,请重新输入!" << endl;
}
else {
break;
}
}
// 输入性别
while (true) {
cout << "请输入性别:";
cin >> person.gender;
// 判断输入性别是否合法
if (person.gender == "男" || person.gender == "女") {
break;
}
cout << "性别不合法,请重新输入!" << endl;
}
// 输入身份证号
while (true) {
cout << "请输入身份证号:";
cin >> person.idNum;
// 判断输入身份证号是否合法
if (idNumIsLegal(person.idNum)) {
break;
}
cout << "身份证号不合法,请重新输入!" << endl;
}
// 输入航班号
while (true) {
cout << "请输入航班号:";
cin >> person.myAirNum;
// 判断 航班号是否合法 且 航班存在 且 人数未满
if (person.myAirNum[0] == 'x' && person.myAirNum.length() == 4
&& airplaneInfo.find(person.myAirNum) != airplaneInfo.end()
&& stringToInt(airplaneInfo[person.myAirNum].tickets) != 0) {
// 购买当前航班,则该航班票数应该减一
int tickets = stringToInt(airplaneInfo[person.myAirNum].tickets); // 转化为整形
tickets--; // 票数减一
airplaneInfo[person.myAirNum].tickets = to_string(tickets); // 转化为字符串
updateAirplaneFile(); // 更新航班文件
break;
}
cout << "航班号不合法或者该航班不存在或者航班人数已满,请重新选择!" << endl;
}
// 2,用户信息存入userInfo中
string key = person.idNum.substr(13); // 获取该用户对应的key值:身份证号后六位
userInfo.insert(pair<string, Person>(key, person)); // 将用户信息存入map中
// 3,新增用户信息写入文件操作
fstream fs;
fs.open("userInfo.txt", ios::out | ios::app); // 打开文件
// 写入文件
fs << person.name + " " << person.age + " "
<< person.gender + " " << person.idNum + " "
<< person.myAirNum + " " << endl;
cout << "购票成功!!" << endl;
fs.close();
}
显示输出用户信息
/// <summary>
/// 显示输出用户信息
/// </summary>
void showUserInfo() {
// 判断用户信息是否为空
if (userInfo.empty()) {
cout << "用户信息为空!!" << endl;
return;
}
// 若用户信息不为空开始遍历
map<string, Person>::iterator it; // 迭代器
for (it = userInfo.begin(); it != userInfo.end(); it++) {
cout << "姓名:" + it->second.name
<< " 年龄:" + it->second.age
<< " 性别:" + it->second.gender
<< " 身份证号:" + it->second.idNum
<< " 航班号:" + it->second.myAirNum
<< " 起点:" + airplaneInfo[it->second.myAirNum].startPoint
<< " 终点:" + airplaneInfo[it->second.myAirNum].destination
<< " 票价:" + airplaneInfo[it->second.myAirNum].ticketPrice
<< " 起飞时间:" + airplaneInfo[it->second.myAirNum].departureTime
<< endl;
}
}
示输出航班信息
/// <summary>
/// 显示输出航班信息
/// </summary>
void showAirplaneInfo() {
// 如果航班信息为空
if (airplaneInfo.empty()) {
cout << "航班信息为空!" << endl;
return;
}
// 若航班信息不为空开始遍历
map<string, Airplane>::iterator it; // 迭代器
int i = 0;
for (it = airplaneInfo.begin(); it != airplaneInfo.end(); it++) {
cout << "航班号:" + it->second.airNum
<< " 起点:" + it->second.startPoint
<< " 终点:" + it->second.destination
<< " 票数:" + it->second.tickets
<< " 票价:" + it->second.ticketPrice
<< " 起飞时间:" + it->second.departureTime
<< endl;
}
}
查询用户信息
map直接找就行;
/// <summary>
/// 查询用户信息
/// </summary>
/// <param name="idNum">查询用户的身份证号</param>
void selectUserInfo(string idNum) {
string key = idNum.substr(13); // 获取key值:身份证号后六位
// 判断用户信息是否存在
if (userInfo.find(key) == userInfo.end()) {
cout <<"该用户不存在!!" << endl;
return ;
}
Person person = userInfo[key]; // 通过key查找用户信息
// 输出用户信息
cout << "姓名:" + person.name
<< " 年龄:" + person.age
<< " 性别:" + person.gender
<< " 身份证号:" + person.idNum
<< " 航班号:" + person.myAirNum
<< " 起点:" + airplaneInfo[person.myAirNum].startPoint
<< " 终点:" + airplaneInfo[person.myAirNum].destination
<< " 票价:" + airplaneInfo[person.myAirNum].ticketPrice
<< " 起飞时间:" + airplaneInfo[person.myAirNum].departureTime
<< endl;
}
退票
退票不仅要在内存中删除用户信息,文件中也要删除;但是文件操作并没有直接删除的功能,所以当内存中的用户信息删除后,把所有文件信息清空,然后再重写把内存中的用户信息写入文件就可以了;
/// <summary>
/// 退票
/// </summary>
/// <param name="idNum">退票用户身份证号</param>
void refundTickets(string idNum) {
// 判断该用户是否存在
string key = idNum.substr(13);
// 如果不存在
if (userInfo.find(key) == userInfo.end()) {
cout << "用户信息不存在!" << endl;
return;
}
// 如果存在,该航班票数需要增加一
int tickets = stringToInt(airplaneInfo[userInfo[key].myAirNum].tickets); // 字符串转整型
tickets++; // 票数加一
airplaneInfo[userInfo[key].myAirNum].tickets = to_string(tickets); // 整型转字符串
updateAirplaneFile(); // 更新航班文件
userInfo.erase(key); // 从用户信息中删除
// 从文件中删除该用户信息
clearFile("userInfo.txt"); // 清空文件
// 重新写入文件
fstream fs;
fs.open("userInfo.txt", ios::out | ios::app); // 打开文件
// 遍历重新写入用户信息
for (auto i : userInfo) {
fs << i.second.name + " "
<< i.second.age + " "
<< i.second.gender + " "
<< i.second.idNum + " "
<< i.second.myAirNum << endl;
}
fs.close();
cout << "退票成功!!" << endl;
}
二、工具函数
函数声明:
vector<string> split(const string& str, const string& delim); // 字符串分割函数
bool idNumIsLegal(string idNum); // 判断身份证号是否合法
bool nameIsLegal(string name); // 判断姓名是否合法
int stringToInt(string str); // 将string类型转化为int类型
void clearFile(const string fileName); // 清空文件内容
void updateAirplaneFile(); // 更新航班文件信息
字符串分割函数
因为C++没有split函数,但是可以通过C语言的strtok函数实现相应的功能,于是我自行封装了一个字符串分割函数;
/// <summary>
/// 字符串分割函数
/// </summary>
/// <param name="str">需要的分割的字符串</param>
/// <param name="delim">分割标准</param>
/// <returns>字符串数组</returns>
vector<string> split(const string& str, const string& delim) {
vector<string> res; // 存放分割字符串的数组
if ("" == str) return res; // 字符串为空则无法分割
// 先将要切割的字符串从string类型转换为char*类型
char* strs = new char[str.length() + 1];
strcpy(strs, str.c_str());
char* d = new char[delim.length() + 1];
strcpy(d, delim.c_str()); // 将delim转化为char*赋给d
char* p = strtok(strs, d); // 首次调用指向分解字符串
while (p) {
string s = p; // 分割得到的字符串转换为string类型
res.push_back(s); // 存入结果数组
p = strtok(NULL, d); // 之后每一次分割,分解字符串处为NULL
}
return res;
}
判断身份证号是否合法
其实判断身份证号合法只需要把身份证号分为几部分分别判断就可以了;
但是这样有点麻烦,可以选择更简单的正则表达式实现判断,正好C++也支持正则表达式,所以我就网上找到了身份证号的正则表达式直接用就好了;
/// <summary>
/// 判断身份证号是否合法
/// </summary>
/// <param name="idNum">身份证号</param>
/// <returns>合法为true,非法为false</returns>
bool idNumIsLegal(string idNum) {
// 定义一个 正则表达式 为18位身份证号码的判定规则
regex repPattern("^[1-9]\\d{5}(18|19|([23]\\d))\\d{2}((0[1-9])|(10|11|12))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$");
// 声明匹配结果变量
match_results<string::const_iterator> rerResult;
// 进行匹配
bool bValid = regex_match(idNum, rerResult, repPattern);
if (bValid) {
// 匹配成功
//cout << "身份证号格式合法!" << endl;
return true;
}
//cout << "身份证号格式不合法!" << endl;
return false;
}
判断姓名是否合法
和身份证号判断一样,直接使用正则表达式;
/// <summary>
/// 判断姓名是否合法
/// </summary>
/// <param name="name">姓名</param>
/// <returns>合法为true,非法为false</returns>
bool nameIsLegal(string name) {
// 定义一个 正则表达式 为18位身份证号码的判定规则
regex repPattern("^[\u4e00-\u9fa5]+(·[\u4e00-\u9fa5]+)*$");
// 声明匹配结果变量
match_results<string::const_iterator> rerResult;
// 进行匹配
bool bValid = regex_match(name, rerResult, repPattern);
if (bValid) {
// 匹配成功
//cout << "姓名格式合法!" << endl;
return true;
}
//cout << "姓名格式不合法!" << endl;
return false;
}
将string类型转化为int类型
这个使用的是stoi函数,不了解的可以看一下我的这篇文章:C++中stoi(),atoi() ,to_string()使用技巧
/// <summary>
/// 将string类型转化为int类型
/// </summary>
/// <param name="str">需转化的字符串</param>
/// <returns>该字符串转化后的整型</returns>
int stringToInt(string str) {
// 判断字符串是否为空
if (str.empty()) {
cout << "数据出现错误,请检查文件数据!" << endl;
return 0;
}
return stoi(str); // 字符串转化为整型
}
清空文件内容
/// <summary>
/// 清空文件内容
/// </summary>
/// <param name="fileName">文件名</param>
void clearFile(const string fileName) {
fstream file(fileName, ios::out);
}
更新航班文件信息
这个就是和更新用户信息一样的,就是删除了重写,因为多次用到就单独写成函数了;
/// <summary>
/// 更新航班文件信息
/// </summary>
void updateAirplaneFile() {
// 从文件中删除
clearFile("airplane.txt"); // 清空文件
// 重新写入文件
fstream fs;
fs.open("airplane.txt", ios::out | ios::app); // 打开文件
// 遍历重新写入航班信息
for (auto i : airplaneInfo) {
fs << i.second.airNum + " "
<< i.second.startPoint + " "
<< i.second.destination + " "
<< i.second.tickets + " "
<< i.second.ticketPrice + " "
<< i.second.departureTime + " "
<< endl;
}
fs.close();
}
总结
因为这个项目功能实现很简单,代码实现起来并没有那么难;其实一个项目的开始我感觉最不简单的就是从0到1的过程,当整个框架有了之后其实就没有什么难度了;
缺点
其实用户信息的设计还是存在问题的,就是如果获取了身份证号,那么就可以直接获取到用户的性别和年龄了,这两个变量就是多余的了,所以如果代码进一步的改进的化,我会优先修改这一点;这也是最初设计存在的问题,结果到最后才想到,这时如果进行修改那么修改内容可就很多了;
对我来说这也是一个教训,最初构思设计一定要保证严谨,这样才能避免后期大规模的修改;
目前还没想到其他问题,如果你有其他发现或者有新奇的想法也欢迎来交流;
代码网盘链接
这里就放一个代码和文件的获取链接,我使用的软件是vs2019,有些gcc老版本可能无法识别正则表达式,所以你可以自行更新gcc版本或者使用vs2019即以上版本;
百度网盘:
链接:https://pan.baidu.com/s/1BkLI-FhpJ05O2sTMUuf6cw
提取码:xxxx
欢迎大家的点评!