重温 C 语言课程设计 Shepard-Wang

昨天有个妹子加我微信,说今天她要答辩,希望我帮她看一下课程设计的代码。

这个妹子很心急,“咣咣咣”把代码的七八个函数给我发了过来(这是一个工人的信息管理系统),然后问我“为什么 delete (删除工人信息)函数按照工人的职工号删除职工信息提示成功,但是 list (显示工人信息)函数还是可以打印出来被删除的工人信息?”。

我看了一会一段一段的代码,告诉她,把整个文件发给我。她说“我整理一下代码,然后给你发”。

期间我审视了一下这个代码:不明确的命名,混乱的缩进,乍一看感觉功能很丰富。我感觉这是她抄来的代码,询问她,她也承认了。然后我告诉她“如果你真的想知道这个问题的答案,你明天答辩完,然后来问我,我给你讲解。”

今天晚上我从 7 点多回到家,一直坐在这里修改这个代码,现在是 23 点 35 分。

仔细看这个代码,和网上直接拷贝下来的无异。里面穿插着提示语“请输入学号”和“按职工号打印”,struct workertypedef 成了 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 函数

因此,我们在 freadfwrite 之间调用 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 。怪我,学习去咯。