员工管理系统
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)
{
//如果年大就直接返回1
if (((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);
}
}