一、需要实现的功能
1、球员录入:可以将球员的相关信息录入系统,如球员姓名、国家,进球数等。
2、球员信息存储 :对于录入的球员信息,可以保存至文件中。
3、球员信息读取:可以实现将文件中的球员信息输出。
4、球员信息修改:可以修改任一球员的任何信息。
5、球员信息删除:可以删除指定的任一球员信息。
6、球员查询:可以查询指定的任一球员信息。
7、球员排序: 可以根据球员的某一项数据进行排序。
8、系统界面:用户可以通过系统界面来进行操作。
9、登录系统:可以判断用户的输入密码是否正确,账户是否存在。
二、基本创建思路
基本框架利用链表结构,通过链表的增删查改来实现球员信息的录入和修改。对球员信息的保存,利用fwrite函数来储存,利用fread函数来读取,用data文件类型来储存文件。查询和排序利用链表的遍历和排序算法实现。而系统界面则用printf函数来打印输出。
这是基本界面:
三、具体创建过程及代码
1、球员信息的录入
球员信息的录入实际就是链表的创建过程,每录入一个球员,就需要在内存中开辟新的结点(一个球员信息的空间),并将这个球员结点与之前的结点相连,实现链表的创建。
(1)头文件的创建和球员结构体的创建
对于头文件创建的原因,主要还是因为主函数之外还需创建许多其它函数,而这些函数许多与链表有关,需要与头结点建立关联。如果而如果在主函数中创建头结点,其他函数如果创建在主函数之前,则无法与头结点产生关联(其它函数编译时,头结点还未创建);而如果其他函数创建在主函数之后,则主函数中调用其他函数就成了问题。为了解决这个矛盾,采用的一种方法是将结构体的创建和头结点的声明放在自己的头文件当中,方便后续的调用和函数的创建。不过最简单的方法还是先声明然后再写函数。这样也不会出现这个问题。
这是头文件:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>struct playerdata{char name[20] ;int age;char country[20];int goal;int help;int yellow;char club[20];
};struct player { //创建球员结点struct playerdata pla;struct player*next;};struct player*head = (struct player*)malloc(sizeof(struct player));//开辟头结点的空间内存
为了方便后面的文件读取和写入,定义了一个新的结构体pla来代表结点的数据域,同时进行了头结点的创建。
(2)录入信息
在信息的录入过程中,有一个很常见的问题。那就是多个scanf函数的输入问题,在第一个scanf结束后,用户敲入的\n回车键会保留在缓冲区中,从而导致后面的scanf无法正常录入,这里采用的方法是利用缓冲区清理函数 fflush( stdin );来解决。并使用了一个for循环来实现多个输入。
录入函数代码:
void inputplayer() {//创建链表以录入信息//打印表头system("cls");printf("\t\t>>> 卡塔尔世界杯球员管理系统 <<<\n");printf("\t******************************************************************\n");printf("请输入要录入球员的个数\n");int n;scanf("%d", &n);//输入要录入的球员个数for (int i = 1; i <= n; ++i) {//打印表头system("cls");printf("\t\t>>> 卡塔尔世界杯球员管理系统 <<<\n");printf("\t******************************************************************\n");struct player*p = (struct player*)malloc(sizeof(struct player));//创建一个临时变量p,用于创建链表的过程p->next = NULL;//头插法创建链表if (head == NULL)head = p;else {p->next = head;head = p;};//录入数据printf("请输入第%d个球员的数据\n", i);printf("姓名:\n");fflush( stdin );scanf(" %s", p->pla.name);printf("年龄:\n");fflush( stdin );scanf("%d", &p->pla.age);printf("国家:\n");fflush( stdin );scanf(" %s", p->pla.country);printf("进球数\n");fflush( stdin );scanf("%d", &p->pla.goal);printf("助攻数\n");fflush( stdin );scanf("%d", &p->pla.help);printf("黄牌数:\n");fflush( stdin );scanf("%d", &p->pla.yellow);printf("球员所属俱乐部\n");fflush( stdin );scanf(" %s", p->pla.club);}saveplayer();//保存至文件printf("球员信息录入成功!\n");return;
};
2、球员信息的输出
球员信息录入完成后,对应的自然是信息的输出,输出时利用链表遍历循环输出
void printplayer() {//打印链表system("cls");printf("\t\t>>> 卡塔尔世界杯球员管理系统 <<<\n");printf("\t**************************************************************\n");printf("\t 姓名\t\t年龄\t国家\t进球数\t助攻数\t黄牌数\t俱乐部\n");printf("\n");struct player*p1;p1 = head;while (p1 ->next != NULL) { //遍历输出链表printf("\t%8s\t%d\t%s\t%3d\t%3d\t%3d\t%s\n",p1->pla.name,p1->pla.age,p1->pla.country,p1->pla.goal,p1->pla.help,p1->pla.yellow,p1->pla.club);printf ("\n");p1 = p1->next;}printf("\t**************************************************************\n");system("pause");
}
输出结果如下:
3、将球员信息保存至文件
(1)文件的打开
对于基本的文件操作,首先需要做的是文件的打开,对于文件的打开,使用fopen函数,该函数可以打开指定位置的文件,同时如果指定位置没有文件,则会创建一个新的文件。该函数格式为fopen(“文件位置”,“打开方式”),返回值为文件的首地址,即一个文件指针。如果打开失败(如在他人的操作环境中),则会返回空指针NULL。利用这个特点可以用于判断文件是否打开。而储存的文件类型选择的是date类型,该类型可以将结构体储存,由于先前创建的结点中数据域为结构体,因此选择该文件类型。
对于文件的位置,可以写C://player.date这样的绝对路径,但这样存在一定缺陷,如果在其他人的操作环境下可能无法打开数据文件,因此这里采用了相对路径,相对路径会直接创建在源代码所在的文件中,使得在他人的操作环境中也可以顺利的创建和打开文件。
这里的打开方式为写入,将信息写入文件:
FILE*fp;fp = fopen("palyer.data", "w");if (fp == NULL)printf("保存时打开文件失败!\n");
(2)文件的关闭
文件的打开对应的就是文件的关闭,因为操作系统对于编程软件的同时打开文件数量有限制,通常不会超过20个,因此我们每次打开文件后都需要关闭文件。
文件的关闭用fclose函数,该函数格式,fclose(“要关闭的文件指针””),返回值如果为0,说明关闭成功,返回EOF(即为-1),则说明关闭失败。
代码如下:
这里提前定义了一个整形变量用于判断fclose函数的返回值
//关闭文件i = fclose(fp);if (i == 0)printf("数据文件已更新!\n");elseprintf("保存时关闭文件失败!\n");
(3)将球员信息保存至文件
这里保存至文件利用fwrite函数,同样是因为该函数可以将结构体写入文件。fwrite函数格式为fwrite(要写的结构体地址,每次书写的长度,书写次数,要写入的文件对应的文件指针),对于书写长度和次数,只要两者相乘为要书写的结构体大小即可,这里用的是每次书写1个字节,书写结构体的大小的次数(通过sizeof函数得到)。将头结点赋临时创建的结点,然后遍历写入每个结点的数据域。
代码如下:
struct player *p3;p3 = head;//遍历链表录入文件while (p3->next != NULL) {fwrite(&p3->pla, 1, sizeof(struct playerdata), fp);p3 = p3->next;};
4、文件的读取
文件的读取用该与fwrite相对应fread函数。在使用文件的保存和读取时,一般需要两个函数对应,如fwrite和fread,fprintf和fscanf等。fread函数同样也可以直接读取结构体。读取的思路实际与录入的思路一致,区别是录入的数据是用户自己输入的,而读取的数据是利用文件读取函数获得的,我们只需要将文件读取的结果赋给结点即可。具体为新建立一个与头结点相连的链表,将文件中的球员信息录入到链表中,实现文件数据转移至链表,输出时也只需要输出链表。
而具体的过程则是首先框架是一个大的循环,循环内在一个结点一个结点的读取,直到读完为止。
循环内的思路为先创建一个新的结点,让fread函数一次读取一个结点的数据域大小,然后将读取结果赋值给新建立的结点,然后再利用头插法或尾插法将结点连接。
循环判断的条件利用fread函数的返回值,其返回值为读取的基本单元个数(就是每次读取的字节数),当读取的单元数为0时,说明已经读取完成。
可能出现的问题:
读取时容易出现的问题就是乱码,其原因可能是由于二进制文件的读取方式。这里解决的思路是新建立一个先前的数据域结构体,将读取结果赋给这个结构体,然后利用memcpy函数复制指定大小的字节,这样就可以把乱码部分去掉。
具体代码如下:
FILE*fp;fp = fopen("palyer.data", "r");if (fp == NULL)printf("打开信息文件失败!\n");struct playerdata play;//循环储存信息while (fread(&play, 1, sizeof(struct playerdata), fp)) {struct player*p = (struct player*)malloc(sizeof(struct player));p->next = NULL;memcpy(p, &play, sizeof(struct playerdata));if (head == NULL)head = p;else {p->next = head;head = p;}}//关闭文件int j = fclose(fp);if (j != 0)printf("关闭信息文件失败!\n");system("pause");
5、球员信息的修改
链表结构的修改思路是先遍历找到要修改的指定结点,然后直接输入新的对应的数据即可。
这里容易出现的问题是判断是否为要修改的结点时,通常用户输入的为姓名,国家之类的字符组,而判断时如果只是简单的利用“==”来判断是否为指定结点,大概率会出现信息存在却无法查询到的情况,这里需要利用strcmp函数,该函数可以用于比较字符串,如果相等会返回0。将判断条件改为strcmp函数结果是否为0可以解决。
具体细节在代码中可以看到:
system("cls");printf("\t\t>>> 卡塔尔世界杯球员管理系统 <<<\n");printf("\t******************************************************************\n");printf("请输入要修改数据的球员姓名:");char name1[20];//定义一个数组来储存输入的姓名scanf(" %s", name1);struct player* p = head;while (p != NULL) {if (0 == strcmp( p->pla.name, name1) ) {//打印提示信息printf("该球员的信息有如下类别\n");printf("\t\t\t***************************\n");printf("\t\t\t* *\n");printf("\t\t\t* 1、姓名 *\n");printf("\t\t\t* *\n");printf("\t\t\t* 2、年龄 *\n");printf("\t\t\t* *\n");printf("\t\t\t* 3、国家 *\n");printf("\t\t\t* *\n");printf("\t\t\t* 4、进球数 *\n");printf("\t\t\t* *\n");printf("\t\t\t* 5、助攻数 *\n");printf("\t\t\t* *\n");printf("\t\t\t* 6、黄牌数 *\n");printf("\t\t\t* *\n");printf("\t\t\t* 7、俱乐部 *\n");printf("\t\t\t* *\n");printf("\t\t\t***************************\n");printf("请选择要修改的信息:");//选择要修改的信息int chance;scanf("%d", &chance);switch (chance) {case 1 :printf("请输入新的姓名:");scanf("%s", p->pla.name);break;case 2 :printf("请输入新的年龄:");scanf("%d", &p->pla.age);break;case 3 :printf("请输入新的国家:");scanf("%s", p->pla.country);break;case 4 :printf("请输入新的进球数:");scanf("%d", &p->pla.goal);break;case 5 :printf("请输入新的助攻数:");scanf("%d", &p->pla.help);break;case 6 :printf("请输入新的黄牌数:");scanf("%d", &p->pla.yellow);break;case 7 :printf("请输入新的所在俱乐部:");scanf("%s", p->pla.club);break;default: {printf("您的输入有误,请重新输入\n");break;}};break;}p = p->next;}//结束提示信息if (p == NULL) {printf ("\n");printf("该球员未参赛!\n");printf ("\n");} else {printf ("\n");printf("修改完成!\n");printf ("\n");}
代码看起来很长,这是因为为了让用户可以更加容易地修改指定信息,所以做了一个选择界面和switch函数来选择,实际的核心代码只有if判断的函数。
这是输出的界面:
6、球员信息的删除
基本思路:链表的删除操作思路为找到要删除的结点的上一个结点,然后把这个结点与要删除的结点的下一个结点连接,这样就绕过的这样要删除的结点,最后释放这个要删除的结点即可。
这里可能出现的问题有三个。
第一个是要判断需删除的结点是否为头结点,因为如果是头结点的话是不存在上一个结点的,这样会导致无法找到结点,产生BUG。解决的方法就是先判断是否为头结点,如果是就让头结点的下一个成为头结点,然后释放之前的头结点。
代码如下:i = 1是用于后判断是否没有可以删除的结点
if (0 == strcmp(head->pla.name, name1)) {p1 = head;head = head -> next;free(p1);i = 1;
第二个是循环遍历的判断条件,由于我们需要找到要删除的结点的上一个结点,所以在判断时判断的实际为遍历到当前结点的下一个结点是否为要删除的结点。一般的遍历判断条件为结点p != NULL,而如果结点P遍历到最后一个结点时,这时判断的是P的下一个结点,而P 为最后一个结点,其下一个结点为NULL,这时判断时很容易出错,会出现无法判断或者输出乱码的情况。
解决的办法是将遍历的条件改为P -> next != NULL,这时能遍历到的最后一个结点是倒数第二个结点,这时判断的最后一个结点就成了最后一个结点,这样就避免了判断出错。
代码如下:
else {p2 = head;//遍历链表while (p2->next != NULL) {if (0 == strcmp (p2->next->pla.name, name1)) {p3 = p2->next;p2 -> next = p3 -> next;free(p3);i = 1;}p2 = p2 -> next;}}
第三个是判断是否系统中没有要删除的球员。如果判断是否没有要删除的球员时,判断条件为程序是否一直遍历到了最后。由于遍历时最后一个结点为倒数第二个结点,当要删除的球员为最后一个球员时,删除后此时结点便成了最后一个结点,此时也链表遍历到了最后,就会出现已经删除了结点却还会提示 “没有要删除的球员” 。
解决办法是新定义一个整形变量为0,如果找到要删除的结点,就让i = 1,在判断是否没有要删除的球员的条件改为 !i ,当!i为真时,说明 i 为假,则说明没有符合条件的球员。
整体的代码如下:
system("cls");printf("\t\t>>> 卡塔尔世界杯球员管理系统 <<<\n");printf("\t******************************************************************\n");printf("请输入要删除的球员姓名:");int i = 0;char name1[20];scanf(" %s", name1);//输入要删除的球员//定义三个结点用于后续的删除struct player*p1;struct player*p2, *p3;//判断头结点是否为要删除的信息if (0 == strcmp(head->pla.name, name1)) {p1 = head;head = head -> next;free(p1);i = 1;} else {p2 = head;//遍历链表while (p2->next != NULL) {if (0 == strcmp (p2->next->pla.name, name1)) {p3 = p2->next;p2 -> next = p3 -> next;free(p3);i = 1;}p2 = p2 -> next;}}if (!i)printf ("该球员未参赛!\n");if (i)printf("该球员信息删除成功!\n");
7、球员信息的查询
查询的思路与删除基本一致,遍历链表,判断是否为用户输入的信息,如果是的话就将这个结点输出。判断时同样使用了strcmp函数。
代码如下:
system("cls");printf("\t\t>>> 卡塔尔世界杯球员管理系统 <<<\n");printf("\t******************************************************************\n");printf("请输入要查询球员的姓名:\n");int i = 0;char name1[20];scanf(" %s", name1);//输入要查询的姓名printf("\n");struct player *p;//定义一个临时结点用于遍历链表p = head;// 遍历链表while (p->next != NULL) {if (0 == strcmp(p->pla.name, name1)) {//判断是否等于要查询的姓名//打印表头printf("该球员的信息如下:\n");printf("\t******************************************************************\n");printf(" 姓名\t年龄\t国家\t进球数\t助攻数\t黄牌数\t俱乐部\n");//输出信息printf("\t%8s\t%d\t%s\t%3d\t%3d\t%3d\t%s\n",p->pla.name,p->pla.age,p->pla.country,p->pla.goal,p->pla.help,p->pla.yellow,p->pla.club);printf("\t******************************************************************\n");i = 1;}p = p->next;//移动结点}if (!i)//如果遍历结束说明未找到球员printf("该球员未参赛!\n");system("pause");
8、利用进球数或助攻数排序
对于链表的排序,与数组排序不同的是链表排序时数据的遍历不同,数组排序时遍历利用 i ++或者++i,而链表则需要重新建立临时结点, 利用链表遍历,将判断循环结束的条件改为P!=NULL,指针移动改为P = P->next。而其余的排序思想与数组排序基本一致。
排序具体代码如下:
//定义两个临时结点和一个用于临时储存信息的结构体struct player *p1, *p2 ;struct playerdata t;p1 = head;//冒泡排序for (p1 ; p1->next != NULL ; p1 = p1->next) {for (p2 = p1->next ; p2->next != NULL ; p2 = p2 ->next) {if (p1->pla.goal < p2 ->pla.goal) {t = p1->pla;p1->pla = p2->pla;p2->pla = t;}}}
在排序完之后,为了让输出结果更加清晰,且只有进球数,在上一段代码后有加了输出代码
代码如下:
//打印输出struct player*p3;p3 = head;printf("\t\t\t 当前射手排名如下:\n");printf("\t**************************************************************\n");printf("\t\t 姓名\t年龄\t国家\t 进球数\n");while (p3 ->next != NULL) { //遍历输出链表printf("\t\t %8s\t%d\t%s\t%4d\n",p3->pla.name,p3->pla.age,p3->pla.country,p3->pla.goal);p3 = p3->next;}printf("\t**************************************************************\n");system ("pause");
这样最后结果如下:
9、登录系统
这里的登录系统比较简单,只是用来一个if...else的嵌套,可以实现基本输入错误提示,密码输入成功后进入主函数。当返回值为1时进入主函数。
具体代码如下:
int loginplayer() {char account [20];//定义一个字符数组用于储存用户输入的账户char password [20];//定义一个字符数组用于储存用户输入的密码char account1[20] = {"admin"};//提前设置的账户char password1[20] = {"admin"};//提前设置的密码int i = 0;//判断账户输入是否正确printf ("\n");printf("\t\t\t\t账号:");scanf ("%s", account );if (0 == strcmp(account, account1)) {//判断密码输入是否正确printf ("\t\t\t\t密码:");scanf ("%s", password);if (0 == strcmp(password, password1)) {i = 1;printf("登陆成功!\n");} elseprintf ("您输入的密码有误,请重新输入!\n");} elseprintf ("你输入的账户不存在!\n");return i;
}
四、结尾
这里只提供了基本的创建思路,可以在这个基础上加入许多的功能,例如奖项设置,金靴奖,金手套奖,也可以加入注册功能、录入比赛结果等功能。只要基本的功能实现后,后续的功能只需调用基本函数即可,编程速度会大大提高。
文章难免有问题,如果发现问题,欢迎留言与我一起讨论。