员工管理系统
1. 题目要求
设计一个计算机程序,能够实现简单的员工管理功能。
- 每个员工的信息包括:编号、姓名、性别、出生年月、学历、职务、电话、住址等。
- 系统的功能包括:
- 文件操作:将数据输出到文件中以及从文件中加载数据
- 查询:按特定条件查找员工。
- 修改:按编号对某个员工的某项信息进行修改。
- 插入:加入新员工的信息。
- 删除:按编号删除已离职员工的信息。
- 排序:按特定条件对所有员工的信息进行排序。
2. 结构定义
2.1 员工结构定义
// 生日结构
typedef struct birthday
{int year;int month;int day;
}birthday;// 员工结构
typedef struct employee
{int id;char name[50];char gender[10];birthday birthday;char qualification[20];char job[30];char teleNum[15];char location[50];
}employee;
2.2 存储结构定义
typedef struct employee employee;
// 存储结构定义
typedef employee SLDataType;
typedef struct SeqList_dynamic
{SLDataType* SeqList;//指向可以修改大小的数据空间int size;//有效数据个数int capacity;//数据空间的总大小
}SL;//动态顺序表的实现
//初始化和销毁
void SLInit(SL* ps);
void SLDestroy(SL* ps);
//顺序表的扩容
void SLCheckCapacity(SL* ps);
//顺序表的头部插⼊删除 / 尾部插⼊删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//顺序表指定位置之前插⼊/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
3. 项目文件
在本项目中,一共有五个文件
SeqList.h
:存放动态顺序表功能声明SeqList.c
:存放动态顺序表功能定义employee.c
:存放员工管理系统功能声明employee.h
:存放员工管理系统功能定义test.c
:测试文件
4. 项目功能
本项目主要实现下面的功能:
- 加载上次程序结束时保存到文件的员工信息
- 从控制台输入员工信息
- 查找指定信息的员工:仅包括id和姓名查找
- 修改员工的信息
- 插入一个新员工
- 删除指定员工
- 对员工信息按照指定信息排序:仅包括id、姓名和生日
- 打印所有员工信息
- 向文件中写入员工信息数据,便于下次读取
程序主菜单
void menu()
{printf("*****************************\n");printf("******员工信息管理系统*******\n");printf("*1. 输入员工信息\n");printf("*2. 查找员工\n");printf("*3. 修改员工\n");printf("*4. 插入员工\n");printf("*5. 删除员工\n");printf("*6. 排序输出员工信息\n");printf("*7. 查看所有员工信息\n");printf("*8. 退出系统\n");printf("*****************************\n");
}
程序功能对应函数
// 程序菜单
void menu();// 导入数据
void LoadEmployee(SL* employees);// 初始化——输入
void employeeInfoGet(SL* employees);// 查找员工
// 查找方式菜单
void menuForFind();
// 查找员工——返回下标
int findEmployee(SL* employees);
// 查找员工——显示对应员工信息
void findEmployee_print(SL* employees);// 修改菜单
void menuForModify();
// 修改员工信息
void modifyEmployee(SL* employees);// 插入员工信息
void insertEmployee(SL* employees);// 删除员工信息
void deleteEmployee(SL* employees);// 排序菜单
void menuForSort();
// 按照指定内容排序
void sortEmployees(SL* employees);
// 打印排序后的内容
void printSortedEmp(employee* tmp, int size);// 向文件中写入数据
void writeIntoFile(SL* employees);// 销毁系统数据
void destroyEmployee(SL* employees);// 打印所有员工信息
void printEmployees(SL* employees);
程序主函数
#define _CRT_SECURE_NO_WARNINGS 1#include "SeqList.h"
#include "employee.h"int main()
{SL employees;SLInit(&employees);char ans = 0;printf("是否需要导入上次的数据:y/n");scanf(" %c", &ans);if (ans == 'y'){LoadEmployee(&employees);}menu();printf("请输入选项:");int choice = 0;while (scanf("%d", &choice)){switch (choice){case 1:employeeInfoGet(&employees);break;case 2:findEmployee_print(&employees);break; case 3:modifyEmployee(&employees);break;case 4:insertEmployee(&employees);break;case 5:deleteEmployee(&employees);break;case 6:sortEmployees(&employees);break;case 7:printEmployees(&employees);break;case 8:destroyEmployee(&employees);printf("谢谢使用");return 1;default:printf("请按照菜单重新输入\n");break;}menu();printf("请输入选项:");}
}
5. 功能实现
5.1 加载数据
在加载数据函数中,使用fopen
函数和fread
函数进行文件操作,当fread
文件读到一组数据时,向顺序表中插入一组数据,再循环读取直到读到文件结尾
// 导入数据
// 加载上一次的数据
void LoadEmployee(SL* employees)
{FILE* pf = fopen("employees.txt", "rb"); if (pf == NULL) {perror("fopen error!\n"); return;}employee tmp;while (fread(&tmp, sizeof(employee), 1, pf)){SLPushBack(employees, tmp);}fclose(pf);printf("成功导入历史数据\n");
}
5.2 输入数据
首先通过num
控制输入的员工个数
int num = 0;
printf("请输入员工个数:");
scanf("%d", &num);
接着通过for
循环根据num
的值控制数据的输入,对于生日的输入来说,为了保证生日日期的合法性,使用if
语句和goto
语句处理不正确的日期,输入完一组数据后,调用顺序表的尾插函数向顺序表中插入数据
for (int i = 0; i < num; i++)
{employee tmp;printf("请输入第%d个员工\n", i + 1);printf("请输入员工id:");scanf("%d", &(tmp.id));printf("请输入姓名:");scanf("%s", tmp.name);printf("请输入员工性别:");scanf("%s", tmp.gender);printf("请按照年月日的顺序以空格间隔输入员工生日:");
setBirth:scanf("%d%*c%d%*c%d", &(tmp.birthday.year),&(tmp.birthday.month),&(tmp.birthday.day));if (tmp.birthday.month < 1 || tmp.birthday.month > 12 ||(tmp.birthday.day < 1 ||tmp.birthday.day > GetMonthDays(tmp.birthday.year, tmp.birthday.month))){printf("请重新输入生日\n");goto setBirth;}printf("请输入员工学历:");scanf("%s", tmp.qualification);printf("请输入员工职业:");scanf("%s", tmp.job);printf("请输入员工电话:");scanf("%s", tmp.teleNum);printf("请输入员工的住址:");scanf("%s", tmp.location);SLPushBack(employees, tmp);
}
完整模块代码
// 获取日期函数
int GetMonthDays(int year, int month)
{int monthDays[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };if (month == 2 && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))){return monthDays[month] + 1;}return monthDays[month];
}// 获取输入
void employeeInfoGet(SL* employees)
{int num = 0;printf("请输入员工个数:");scanf("%d", &num);for (int i = 0; i < num; i++){employee tmp;printf("请输入第%d个员工\n", i + 1);printf("请输入员工id:");scanf("%d", &(tmp.id));printf("请输入姓名:");scanf("%s", tmp.name);printf("请输入员工性别:");scanf("%s", tmp.gender);printf("请按照年月日的顺序以空格间隔输入员工生日:");setBirth:scanf("%d%*c%d%*c%d", &(tmp.birthday.year),&(tmp.birthday.month),&(tmp.birthday.day));if (tmp.birthday.month < 1 || tmp.birthday.month > 12 ||(tmp.birthday.day < 1 ||tmp.birthday.day > GetMonthDays(tmp.birthday.year, tmp.birthday.month))){printf("请重新输入生日\n");goto setBirth;}printf("请输入员工学历:");scanf("%s", tmp.qualification);printf("请输入员工职业:");scanf("%s", tmp.job);printf("请输入员工电话:");scanf("%s", tmp.teleNum);printf("请输入员工的住址:");scanf("%s", tmp.location);SLPushBack(employees, tmp);}
}
5.3 查找员工信息
在设计查找模块时,一共实现了两种查找方式:
- 查找返回下标
- 查找打印员工信息
5.3.1 查找返回下标
调用返回下标函数便于删除函数调用,一共实现了两种查找方式
- 按照名字查找
// 按照名字查找
int findBy_name(SL* employees, char* target)
{for (int i = 0; i < employees->size; i++){if (strcmp(employees->SeqList[i].name, target) == 0){return i;// 返回对应位置下标}}return -1;
}
- 按照id查找
// 按照id查找
int findBy_id(SL* employees, int target)
{for (int i = 0; i < employees->size; i++){if (employees->SeqList[i].id == target){return i;// 返回对应位置下标}}return -1;
}
调用两种函数的主调函数
// 查找员工——返回下标
int findEmployee(SL* employees)
{int choice = 0;int pos = 0;menuForFind();printf("请选择查找方式:");while (scanf("%d", &choice)){switch (choice){case 1:{int target = 0;printf("请输入需要查找的员工id:");scanf("%d", &target);pos = findBy_id(employees, target);return pos;}break;case 2:{char name[50] = { 0 };printf("请输入需要查找的员工姓名:");scanf("%s", name);pos = findBy_name(employees, name);return pos;}break;case 3:return -1;default:menuForFind();printf("请重新按照菜单输入:");break;}menuForFind();printf("请选择查找方式:");}return -1;
}
5.3.2 查找打印员工信息
调用返回下标的函数,根据返回的pos
下标打印对应的员工信息
// 查找并打印员工信息
void findEmployee_print(SL* employees)
{// 调用findEmployee函数int pos = findEmployee(employees);if (pos != -1){printf("\t员工id:%d", employees->SeqList[pos].id);printf("\t姓名:%s", employees->SeqList[pos].name);printf("\t性别:%s", employees->SeqList[pos].gender);printf("\t生日:%d-%02d-%02d\n", employees->SeqList[pos].birthday.year,employees->SeqList[pos].birthday.month,employees->SeqList[pos].birthday.day);printf("\t学历:%s", employees->SeqList[pos].qualification);printf("\t职业:%s", employees->SeqList[pos].job);printf("\t电话:%s", employees->SeqList[pos].teleNum);printf("\t地址:%s\n", employees->SeqList[pos].location);}else{printf("查无此人\n");}
}
5.4 修改员工信息
实现修改员工信息函数可以选择修改8种信息:
- 编号
- 姓名
- 性别
- 出生日期
- 学历
- 职业
- 电话
- 地址
对于生日日期的输入来说,存在合法日期判断,与输入处理方式类似,结合if
语句和goto
语句进行处理
[!IMPORTANT]
在下面的代码中,对于生日信息的修改使用了整体修改和判断而不是针对年、月或者日进行单独修改,原因如下:
以年为例,如果用户的生日所在年份是闰年,那么对应的2月有29天,但是当用户需要修改年为非闰年时,那么2月的日期也需要修改,此时需要对年、月和日都写判断条件,所以看似是修改一个年份,实际上个别情况需要两个变量都需要修改,所以为了使得修改方便,考虑整体修改再最后整体判断
// 修改员工
void modifyEmployee(SL* employees)
{// 调用查找函数找出需要修改的员工int pos = findEmployee(employees);if (pos == -1){printf("查无此人\n");return;}int choice = 0;menuForModify();printf("请选择需要修改的内容:");while (scanf(" %d", &choice)){switch (choice){case 1:printf("请输入需要修改的id:");scanf("%d", &(employees->SeqList[pos].id));printf("修改完成\n");break;case 2:printf("请输入需要修改的姓名:");scanf("%s", employees->SeqList[pos].name);printf("修改完成\n");break;case 3:printf("请输入需要修改的性别:");scanf("%s", employees->SeqList[pos].gender);printf("修改完成\n");break;case 4:{modifyBirth:printf("请输入修改后的年月日,以空格间隔:");scanf("%d%*c%d%*c%d", &(employees->SeqList[pos].birthday.year),&(employees->SeqList[pos].birthday.month),&(employees->SeqList[pos].birthday.day));if (employees->SeqList[pos].birthday.month < 1 || employees->SeqList[pos].birthday.month > 12 ||(employees->SeqList[pos].birthday.day < 1 ||employees->SeqList[pos].birthday.day > GetMonthDays(employees->SeqList[pos].birthday.year, employees->SeqList[pos].birthday.month))){printf("日期不合法,请重新输入生日\n");goto modifyBirth;}}printf("修改完成\n");break;case 5:printf("请输入需要修改的学历:");scanf("%s", employees->SeqList[pos].qualification);printf("修改完成\n");break;case 6:printf("请输入需要修改的职业:");scanf("%s", employees->SeqList[pos].job);printf("修改完成\n");break;case 7:printf("请输入需要修改的电话:");scanf("%s", employees->SeqList[pos].teleNum);printf("修改完成\n");break;case 8:printf("请输入需要修改的地址:");scanf("%s", employees->SeqList[pos].location);printf("修改完成\n");break;case 9:return;default:printf("请重新选择\n");break;}menuForModify();printf("请选择需要修改的内容:");}
}
5.5 插入员工信息
在插入员工信息函数中,因为与输入员工信息函数基本思路相同,所以考虑直接复用输入员工信息函数
// 插入数据
void insertEmployee(SL* employees)
{// 复用输入函数employeeInfoGet(employees);
}
5.6 删除员工信息
删除员工信息首先需要调用查找到指定员工,再执行删除,如果没找到,则提示“查无此人,删除失败”,否则“删除成功”,因为删除是删除顺序表中的元素,所以直接调用顺序表的删除函数
// 删除员工
void deleteEmployee(SL* employees)
{// 调用查找函数int pos = findEmployee(employees);if (pos < 0){printf("查无此人,删除失败");}SLErase(employees, pos);printf("删除完成\n");
}
5.7 排序员工信息
实现员工的排序信息一共有三种实现方式:
- 按照员工id排序
// 按照id排序
int cmp_id(const void* p1, const void* p2)
{return ((employee*)p1)->id - ((employee*)p2)->id;
}
- 按照员工姓名排序
// 名字比较
int cmp_name(const void* p1, const void* p2)
{return strcmp(((employee*)p1)->name, ((employee*)p2)->name);
}
- 按照员工生日日期排序
// 日期比较
int birthdayCmp(const void* p1, const void* p2)
{//如果年大就直接返回1if (((employee*)p1)->birthday.year > ((employee*)p2)->birthday.year){return 1;}else if (((employee*)p1)->birthday.year == ((employee*)p2)->birthday.year &&((employee*)p1)->birthday.month > ((employee*)p2)->birthday.month)//年相等时比较月份,月份大就直接返回true{return 1;}else if (((employee*)p1)->birthday.year == ((employee*)p2)->birthday.year &&((employee*)p1)->birthday.month == ((employee*)p2)->birthday.month && ((employee*)p1)->birthday.day > ((employee*)p2)->birthday.day)//年相等,月份相等时,天大就直接返回true{return 1;}else//其他情况均返回-1{return -1;}
}
在排序主调函数中,使用C语言库中的qsort
函数结合函数指针调用上述三种函数完成对应的排序功能
调用三种函数的主调函数:
直接调用函数
// 排序主体
void sortEmployees(SL* employees)
{int choice = 0;menuForSort();printf("请选择需要排序的字段:");while (scanf("%d", &choice)){switch (choice){case 1:qsort(employees->SeqList, employees->size, sizeof(employee), cmp_id);printSortedEmp(employees->SeqList, employees->size);break;case 2:qsort(employees->SeqList, employees->size, sizeof(employee), cmp_name);printSortedEmp(employees->SeqList, employees->size);break;case 3:qsort(employees->SeqList, employees->size, sizeof(employee), birthdayCmp);printSortedEmp(employees->SeqList, employees->size);break;case 4:return;default:menuForSort();printf("请重新按照菜单输入:");break;}menuForSort();printf("请选择需要排序的字段:");}
}
转移表优化
// 转移表优化
void sortEmployees(SL* employees)
{int choice = 0;menuForSort();printf("请选择需要排序的字段:");// 定义函数指针数组int (*cmp[4])(const void*, const void*) = {0, cmp_id, cmp_name, birthdayCmp};//cmp arr[4] = {0, cmp_id, cmp_name, birthdayCmp};while (scanf("%d", &choice)){if (choice >= 1 && choice <= 3){qsort(employees->SeqList, employees->size, sizeof(employee), cmp[choice]);printSortedEmp(employees->SeqList, employees->size);}else if (choice == 4){break;}else{menuForSort();printf("请重新输入:");}menuForSort();printf("请选择需要排序的字段:");}
}
为了可以更好看到排序后的结果,在每一次排序结束后,将自动执行一次排序结果打印函数
// 打印排序后的内容
void printSortedEmp(employee* tmp, int size)
{for (int i = 0; i < size; i++){printf("第%d名员工:\n", i + 1);printf("\t员工id:%d", tmp[i].id);printf("\t姓名:%s", tmp[i].name);printf("\t性别:%s", tmp[i].gender);printf("\t生日:%d-%02d-%02d\n", tmp[i].birthday.year,tmp[i].birthday.month,tmp[i].birthday.day);printf("\t学历:%s", tmp[i].qualification);printf("\t职业:%s", tmp[i].job);printf("\t电话:%s", tmp[i].teleNum);printf("\t地址:%s\n", tmp[i].location);}
}
5.8 写入数据与顺序表空间释放
为了更好地保存数据,在程序结束时考虑将内存中的数据使用fopen
和fwrite
函数写入硬盘中
// 向文件中写入数据
void writeIntoFile(SL* employees)
{FILE* pf = fopen("employees.txt", "wb"); if (pf == NULL) {perror("fopen error!\n"); return;}//将通讯录数据写⼊⽂件for (int i = 0; i < employees->size; i++){fwrite(employees->SeqList + i, sizeof(employee), 1, pf);}fclose(pf);printf("数据保存成功!\n");
}
在程序结束前,防止内存泄漏问题,需要对顺序表在堆上的空间进行释放,考虑设计空间销毁函数,因为需要写数据到文件中,所以写入文件过程可以放入空间释放函数中,通过询问用户是否需要保存数据实现更好的交互性
void destroyEmployee(SL* employees)
{char ans = 0;printf("是否需要保存数据:y/n");scanf(" %c", &ans);if (ans == 'y'){writeIntoFile(employees);}SLDestroy(employees);
}
5.9 打印所有员工信息
为了更好让用户看到已经保存到内存缓冲区的内容,考虑使用打印函数将缓冲区内容打印到控制台
// 打印所有员工信息
void printEmployees(SL* employees)
{// 没有员工直接返回if (employees->size == 0){printf("暂无数据\n");return;}for (int i = 0; i < employees->size; i++){printf("第%d名员工:\n", i + 1);printf("\t员工id:%d", employees->SeqList[i].id);printf("\t姓名:%s", employees->SeqList[i].name);printf("\t性别:%s", employees->SeqList[i].gender);printf("\t生日:%d-%02d-%02d\n", employees->SeqList[i].birthday.year,employees->SeqList[i].birthday.month,employees->SeqList[i].birthday.day);printf("\t学历:%s", employees->SeqList[i].qualification);printf("\t职业:%s", employees->SeqList[i].job);printf("\t电话:%s", employees->SeqList[i].teleNum);printf("\t地址:%s\n", employees->SeqList[i].location);}
}