如果这篇博客帮助到你,可以请我喝一杯咖啡~
CC BY 4.0 (除特别声明或转载文章外)
昨天有个妹子加我微信,说今天她要答辩,希望我帮她看一下课程设计的代码。
这个妹子很心急,“咣咣咣”把代码的七八个函数给我发了过来(这是一个工人的信息管理系统),然后问我“为什么 delete (删除工人信息)函数按照工人的职工号删除职工信息提示成功,但是 list (显示工人信息)函数还是可以打印出来被删除的工人信息?”。
我看了一会一段一段的代码,告诉她,把整个文件发给我。她说“我整理一下代码,然后给你发”。
期间我审视了一下这个代码:不明确的命名,混乱的缩进,乍一看感觉功能很丰富。我感觉这是她抄来的代码,询问她,她也承认了。然后我告诉她“如果你真的想知道这个问题的答案,你明天答辩完,然后来问我,我给你讲解。”
今天晚上我从 7 点多回到家,一直坐在这里修改这个代码,现在是 23 点 35 分。
仔细看这个代码,和网上直接拷贝下来的无异。里面穿插着提示语“请输入学号”和“按职工号打印”,struct worker
被 typedef
成了 STU
。我感觉我主要花费的时间不是修改语法错误,而是在还原一个被她修改的“牛头不对马嘴”代码。
现在我们来看一下通过这次修改工人信息管理系统能学到什么。
这是我修改后的 inquiry_worker
(查询工人信息)函数:
void inquiry_worker()
{
int flg;
struct_worker worker;
char tar_name[15];
FILE* fp;
system("cls");
if ((fp = fopen("d:\\file1.txt", "rb")) == NULL)
{
printf("error in func: %s line: %d %s: \n", __func__, __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
printf("\n\t请输入职工的职工号:");
scanf("%s", tar_name);
while (fread(&worker, sizeof(struct_worker), 1, fp) != 0)
{
if (strcmp(tar_name, worker.name) == 0)
{
printf("\n\n\n\t职工号\t姓名\t性别\t职务\t电话\n\n");
printf("\t%s\t%s\t%s\t%s\t%s\n", worker.jobNo, worker.name, worker.gender,worker.position, worker.tel);
flg = 1;
break;
}
}
if (flg == 0)
printf("该职工不存在!");
fclose(fp);
getchar();
printf("\n\tpress any key to continue...");
getchar();
getchar();
}
我将潜在的错误范围缩小,你能发现潜在的错误是什么吗:
while (fread(&worker, sizeof(struct_worker), 1, fp) != 0)
{
if (strcmp(tar_name, worker.name) == 0)
{
printf("\n\n\n\t职工号\t姓名\t性别\t职务\t电话\n\n");
printf("\t%s\t%s\t%s\t%s\t%s\n", worker.jobNo, worker.name, worker.gender,worker.position, worker.tel);
flg = 1;
break;
}
}
其实这里的潜在错误和她犯的错误有相通之处。
这是她给我的时候的 delete 函数:
void del() /*删除函数,完成将文件中的职工数据删除*/
{int i,j,k=0;long del_jobNo;
STU a[15];
FILE *fp;
system("cls");
if((fp=fopen("c:\\file1.txt","rb"))==NULL)
{ printf("error!\n");exit(0); }
printf("\n\n 请输入要删除职工的职工号:\n");
scanf("%ld",&del_jobNo);
while(fread(&a,sizeof(STU),1,fp)!=0)
{ for(i=0;i<n;i++)
if(a[i].jobNo==del_jobNo)
k=1;
break;
}
if(k==0)
printf("该职工不存在!");
else
{for(j=i;j<=n-1;j++)
strcpy(a[j].jobNo,a[j+1].jobNo);
n--;
printf("删除成功!");
}
fclose(fp);
getch();
}
都是只创建一个变量 worker
来不断的从文件 fp
中读取内容。
这样做乍一看感觉很妙,但是它有潜在的危机:
如果一个进程首先不是先调用 input
(初始化工人信息然后输入到文件)函数,而是直接调用 inquiry_worker
函数会发什么?它很可能打开的是上一次程序输入的内容或者其他进程输入的内容。这显然是不合适的。
delete 函数的逻辑是这样的:如果找到了要删除的工号,那么从找到的位置的下一元素(数组)个不断向前覆盖。仔细想一下,这其实并不算删除,只是你能读取到的工人少了一个。那么如果这时候循环调用 fread
函数,因为文件中还是存在你“删除”的内容,fread
还会读取成功。这样,自然还会输出被删除的工人信息(或者最后一个工人信息)。
修改的方式有两个:
- 创建一个数组,一次性用 fread 读完现存工人信息(工人数量用一个全局变量标识)
- 声明一个临时变量指示还有多少工人信息没有读入
我重构后的 inquiry
函数就是采用第一种方式,list_workers
(打印工人信息)的实现采用了第二种方式:
while (cnt-- && (fread(&worker, sizeof(struct_worker), 1, fp) != 0))
printf("\t%s \t%s \t%s \t%s \t%s\n", worker.jobNo, worker.name, worker.gender, worker.position, worker.tel);
下面是我重构后的 delete_worker
函数:
void delete_worker()
{
int i, j, flg = 0;
struct_worker workers[WORKER_MAX], del_jobNo[15];
FILE* fp;
system("cls");
if ((fp = fopen("d:\\file1.txt", "rb+")) == NULL)
{
printf("error in func: %s line: %d %s: \n", __func__, __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
printf("\n\n 请输入要删除职工的职工号: ");
scanf("%s", del_jobNo);
if (fread(workers, sizeof(struct_worker), worker_cnt, fp) != worker_cnt)
{
printf("error in func: %s line: %d %s: \n", __func__, __LINE__, strerror(errno));
exit(EXIT_FAILURE);
}
for (i = 0; i < worker_cnt; i++)
if (strcmp(workers[i].jobNo, del_jobNo) == 0)
{
flg = 1;
break;
}
if (flg == 0)
printf("该职工不存在!");
else
{
for (j = i; j < worker_cnt - 1; j++)
{
strcpy(workers[j].jobNo, workers[j + 1].jobNo);
strcpy(workers[j].gender, workers[j + 1].gender);
strcpy(workers[j].name, workers[j + 1].name);
strcpy(workers[j].position, workers[j + 1].position);
strcpy(workers[j].tel, workers[j + 1].tel);
}
worker_cnt--;
fseek(fp, 0L, SEEK_SET);
fwrite(workers, sizeof(struct_worker), worker_cnt, fp);
printf("删除成功!");
}
fclose(fp);
getchar();
getchar();
}
对比她写的 delete
函数,还有一个重要的错误就是删除后没有将新的工人信息写入文件中。
如果这时候我们直接调用 fwrite
函数,其实会有潜在的错误:因为我们读取完文件后,文件指针已经偏移了文件开始的位置,这时候你文件中写入,并不会从头开始写!
读过 《C 陷阱与缺陷》的人应该知道:文件读操作和写操作之间应该调用 fseek
函数
因此,我们在 fread
和 fwrite
之间调用 fseek
函数,让文件指针回到文件头。
又因为要向文件中写,所以原来打开文件的方式 rb
显然不再合适,那应该用那个访问模式呢?
我们来看一下 cppreference 中对访问模式 mode 的描述:
因为我们首先是要读取文件信息,我们不应该在文件不存在时创建新的文件,所以选择 rb+
细心的朋友应该发现了,为什么函数最下面调用了两次 getchar()
?
因为我需要在这个函数执行完后停留,让用户按下回车,然后再进入下一次的选择。
那么一次 getchar
难道不够吗?
当然,第一个 getchar 接受的是 main 函数中用户输入选择后按下的回车:
switch (choice)
{
case 1: input_workers(); break; /*调用输入函数*/
case 2: list_workers(); break; /*调用显示函数*/
case 3: append_workers(); break; /*调用追加函数*/
case 4: sort_workers(); break; /*调用排序函数*/
case 5: inquiry_worker(); break; /*调用查询函数*/
case 6: delete_worker(); break; /*调用删除函数*/
case 0: exit(0); /*结束程序的运行*/
}
} while (choice != 0);
其实这次修改这个代码还是学习到了东西,我觉的这个代码唯一的亮点就是 打印的界面感觉还是蛮好看的
printf("\n\n\n\n\n\t\t\t\t\t Personnel File Management System\n"); /*显示程序菜单*/
printf("\n\n\t\t\t\t\t*********************************\n\n");
printf("\t\t\t\t\t\t1----输入职工信息\n");
printf("\t\t\t\t\t\t2----显示职工信息\n");
printf("\t\t\t\t\t\t3----追加职工信息\n");
printf("\t\t\t\t\t\t4----排序职工信息\n");
printf("\t\t\t\t\t\t5----查询职工信息\n");
printf("\t\t\t\t\t\t6----删除职工信息\n");
printf("\t\t\t\t\t\t0----退出\n");
printf("\n\t\t\t\t\t*********************************\n");
printf("\n\t\t\t请选择:");
晚上 11 点,当我差不多重构好代码后,当我打算和她交流一下的时候:
“你这代码问题有点多啊”
消息发送失败 :exclamation:
跟我对象说了这件事后,她觉得可能我的态度稍微有点不好。
其实吧,主要是我时间不够,晚上还在忙;再一个,时间够我也没必要给你解决问题啊,你问我的又不是我写的文章中的问题。
现在已经找不到当时的聊天记录了,但是 7,8 个函数是一条消息一条消息发过来的,我可以让大家看一下我重构以前的代码:
#include<stdio.h>
#include<stdlib.h>
typedef struct workers
{
long jobNo; //职工号
char name[15]; //姓名
char sex[15]; //性别
char birthday[15]; //出生年月
char degree[15]; //学历
char position[15]; //职务
int salary; //工资
char addr[15]; //住址
long tel; //电话
}STU;
int n;
void input() /*输入函数,完成将键盘输入的职工数据写入磁盘*/
{
int i;
STU a; /*结构体变量a 用来存放一个职工的数据*/
FILE *fp;
system("cls"); /*清屏*/
if((fp=fopen("c:\\file1.txt","wb"))==NULL)
{ printf("error!\n");exit(0); }
printf("\n\t请输入学生人数:");
scanf("%d",&n); /*输入职工人数*/
for(i=0;i<n;i++) /*以循环的方式输入每个职工的数据*/
{ printf("\n\t请输入第%d名职工信息:\n",i+1);
printf("\t职工号:");
scanf("%ld",&a.jobNo);
printf("\t姓名:");
scanf("%s",a.name);
printf("\t性别:");
scanf("%s",a.sex);
printf("\t出生年月:");
scanf("%s",&a.birthday);
printf("\t学历:");
scanf("%s",&a.degree);
printf("\t职务:");
scanf("%s",&a.position);
printf("\t工资:");
scanf("%d",&a.salary);
printf("\t住址:");
scanf("%s",&a.addr);
printf("\t电话:");
scanf("%ld",&a.tel);
fwrite(&a,sizeof(STU),1,fp);
}
fclose(fp);
}
void list() /*显示函数,完成将文件中的职工数据显示在屏幕上*/
{ STU a; /*结构体变量a 用来存放一个职工的数据*/
FILE *fp;
system("cls");
if((fp=fopen("c:\\file1.txt","rb"))==NULL)
{ printf("error!\n");exit(0); }
printf("\n\n\n\t职工号\t姓名\t性别\t出生年月\t学历\t职务\t工资\t住址\t电话\n\n");
while(fread(&a,sizeof(STU),1,fp)!=0)
printf("\t%ld\t%s\t%s\t%s\t\t%s\t%s\t%d\t%s\t%ld\n",a.jobNo,a.name,a.sex,a.birthday,a.degree,a.position,a.salary,a.addr,a.tel);
fclose(fp);
printf("\n\tpress any key to continue...");
getch();
}
void append() /*追加函数,完成将键盘上输入的职工数据追加到文件中*/
{
int i,m;
STU a;
FILE *fp;
system("cls");
if((fp=fopen("c:\\file1.txt","ab"))==NULL)
{ printf("error!\n");exit(0); }
printf("\n\n\t请输入追加人数:");
scanf("%d",&m);
for(i=0;i<m;i++)
{ printf("\t职工号:");
scanf("%ld",&a.jobNo);
printf("\t姓名:");
scanf("%s",a.name);
printf("\t性别:");
scanf("%s",a.sex);
printf("\t出生年月:");
scanf("%s",&a.birthday);
printf("\t学历:");
scanf("%s",&a.degree);
printf("\t职务:");
scanf("%s",&a.position);
printf("\t工资:");
scanf("%d",&a.salary);
printf("\t住址:");
scanf("%s",&a.addr);
printf("\t电话:");
scanf("%ld",&a.tel);
fwrite(&a,sizeof(STU),1,fp);
}
n=n+m;
fclose(fp);
}
void sort() /*排序函数,完成对文件中的职工数据按职工号排序并显示在屏幕上*/
{ int i,j,n=0;
STU a[15],t; /*结构体数组a 可存放15个职工的数据*/
FILE *fp;
system("cls");
if((fp=fopen("c:\\file1.txt","rb"))==NULL)
{ printf("error!\n");exit(0); }
while(fread(&a[n],sizeof(STU),1,fp)!=0)
n++; /*变量n累计文件中职工人数*/
for(i=0;i<n-1;i++)
for(j=n-1;j>i;j--)
if(a[j].jobNo<a[j-1].jobNo)
{ t=a[j];a[j]=a[j-1];a[j-1]=t; }
printf("\n\n\n\t按职工排序:\n");
printf("\n\t学号\t姓名\t性别\t出生年月\t学历\t职务\t工资\t住址\t电话\n\n");
for(i=0;i<n;i++)
printf("\t%ld\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%ld\n",a[i].jobNo,a[i].name,a[i].sex,a[i].birthday,a[i].degree,a[i].position,a[i].salary,a[i].addr,a[i].tel);
fclose(fp);
printf("\n\tpress any key to continue...");
getch(); /*程序暂停*/
}
void inq() /*查询职工 */
{int j,k=0;
STU a;
FILE *fp;
system("cls");
if((fp=fopen("c:\\file1.txt","rb"))==NULL)
{ printf("error!\n");exit(0); }
printf("\n\t请输入职工的职工号:");
scanf("%ld",&j);
while(fread(&a,sizeof(STU),1,fp)!=0)
{if(j==a.jobNo)
{printf("\n\n\n\t学号\t姓名\t性别\t出生年月\t学历\t职务\t工资\t住址\t电话\n\n");
printf("\t%ld\t%s\t%s\t%s\t%s\t%s\t%d\t%s\t%ld\n",a.jobNo,a.name,a.sex,a.birthday,a.degree,a.position,a.salary,a.addr,a.tel);
k=1;break;
}
}
if(k==0)
printf("该职工不存在!");
fclose(fp);
getch();
}
void del() /*删除函数,完成将文件中的职工数据删除*/
{int i,j,k=0;long del_jobNo;
STU a[15];
FILE *fp;
system("cls");
if((fp=fopen("c:\\file1.txt","rb"))==NULL)
{ printf("error!\n");exit(0); }
printf("\n\n 请输入要删除职工的职工号:\n");
scanf("%ld",&del_jobNo);
while(fread(&a,sizeof(STU),1,fp)!=0)
{ for(i=0;i<n;i++)
if(a[i].jobNo==del_jobNo)
k=1;
break;
}
if(k==0)
printf("该职工不存在!");
else
{for(j=i;j<=n-1;j++)
strcpy(a[j].jobNo,a[j+1].jobNo);
n--;
printf("删除成功!");
}
fclose(fp);
getch();
}
main() /*主函数,完成程序菜单的显示并调用以上各函数*/
{ int a;
do
{ system("cls"); /*清屏*/
printf("\n\n\n\n\n\t\t\t\t\t Personnel File Management System\n"); /*显示程序菜单*/
printf("\n\n\t\t\t\t\t*********************************\n\n");
printf("\t\t\t\t\t\t1----输入职工信息\n");
printf("\t\t\t\t\t\t2----显示职工信息\n");
printf("\t\t\t\t\t\t3----追加职工信息\n");
printf("\t\t\t\t\t\t4----排序职工信息\n");
printf("\t\t\t\t\t\t5----查询职工信息\n");
printf("\t\t\t\t\t\t6----删除职工信息\n");
printf("\t\t\t\t\t\t0----退出\n");
printf("\n\t\t\t\t\t*********************************\n");
printf("\n\t\t\t请选择:");
scanf("%d",&a);
switch(a)
{ case 1:input();break; /*调用输入函数*/
case 2:list();break; /*调用显示函数*/
case 3:append();break; /*调用追加函数*/
case 4:sort();break; /*调用排序函数*/
case 5:inq();break; /*调用查询函数*/
case 6:del();break; /*调用删除函数*/
case 0:exit(0); /*结束程序的运行*/
}
}while(a!=0);
}
你问我 delete 函数删除后 list 函数打印的不对,那我是不是应该看一下程序的主体,再看看这两个函数?
那么现在我问你,晚上 11 点,你在忙,这样的代码,你会看吗?
也许是我菜吧,不能一眼看出来 bug 。怪我,学习去咯。