首先创建三个文件stu.c、stu.h、main.c
因为我们接下来会用到字符串比较、malloc函数以及自定义函数,故需要四个头文件
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include"stu.h"
首先在开始写代码之前,需要明确,我们需要哪些功能,实现这些功能有需要哪些模块。
首先要实现基本的增、删、改、查、以及遍历,我们就需要 “尾插函数”、“删除节点函数”、“查询函数”、”遍历函数“。在插入函数中,我们需要申请节点,故需要“节点申请函数”、“头节点创建函数”;在删除函数中,我们要判断链表是否为空,所以还需要”判空函数“。当我们进行查询时,需要找到前一个节点,故寻址函数也是必需品……
最后的最后,我们也不能忘记将链表销毁
所以我们需要在头文件stu.h中申明以下函数
#ifndef __STU__
#define __STU__
//创建头节点
Linklist *create();
//申请子节点
Linklist *node_add( STU student);
//判空
int empty(Linklist *S);
//遍历
void list_show(Linklist *L);
//增加学生信息(默认尾插,你别无选择)
int list_intsrt_tail(Linklist *S, STU student);
//寻找前一个位置地址
Linklist *find(Linklist *S, Linklist *p);
//任意位置删除
void delete_everypos(Linklist *S, Linklist *p);
//基于姓名删除
int delete_by_name(Linklist *S, char n[]);
//基于学号删除
int delete_by_id(Linklist *S, int n);
//基于姓名修改
int updata_name(Linklist *S, char arr[]);
//基于学号修改
int updata_id(Linklist *S, int id);
//基于姓名查找
int select_by_name(Linklist *S, char n[]);
//基于学号查找
int select_by_id(Linklist *S, int n);
//基于成绩查找
int select_by_score(Linklist *S, float n);
//学号升序
void sort(Linklist *S);
//释放链表
void free_list(Linklist* S);
#endif
然后是创建链表的结构体
我采用的是结构体嵌套共用体,在共用体里用结构体指针指向另一个结构体的方法来创建变量
typedef struct STU
{
char name[10]; //姓名
int id; //学号
float score; //成绩
}STU;
typedef struct Node
{
union
{
STU *info; //学生信息
int len; //链表长度
};
//指针域
struct Node *next;
}Linklist;
用在共用体内定义结构体指针的方法,相比与直接在共用体内定义结构体的方法,虽然以后申请节点要手动申请一次STU结构体的空间,但是这样做相对的会更加节省空间(指头节点省空间)
思路梳理完毕,接下来是各个功能函数的实现
在主函数中,我采取经典的用while(1)实现死循环的手法建立菜单框架。但是因为在增删改查中,有部分模块有多个分支方向,比如,查找中,可以选择通过学号查找,也可以通过姓名查找。于是我经过考虑,最终选择在switc结构里嵌套switch
以下是主函数代码总览
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include"stu.h"
int main(int argc, const char *argv[])
{
//创建链表
Linklist *S = create();
while(1)
{
printf("欢迎使用学生管理系统\n");
printf("请选择您要使用的功能\n");
printf("1:增加学生信息\n");
printf("2:删除学生信息\n");
printf("3:修改学生信息\n");
printf("4:查找学生信息\n");
printf("5:查看全部学生信息\n");
printf("6:学号升序\n");
printf("0:退出\n");
printf("请输入编号>>>");
int n;
scanf("%d",&n);
switch(n)
{
case 1: //增
{
for(int i=0;; i++) //在栈区申请变量,录入成绩
{
STU student;
printf("请输入姓名>>>");
scanf("%s",student.name);
//当输入#时,录入结束
if(strcmp( student.name, "#" ) ==0 )
{
break;
}
printf("请输入学号>>>");
scanf("%d",&student.id);
printf("请输入成绩>>>");
scanf("%f",&student.score);
printf("\n");
//在堆区申请空间,把栈区变量的值录入
list_intsrt_tail(S, student); //调用增函数
}
printf("信息录入完成\n\n");
}break;
case 2: //删
{
printf("1:基于姓名删除\n");
printf("2:基于学号删除\n");
printf("请选择您要使用的功能分支 >>> ");
int n2;
scanf("%d",&n2);
switch(n2)
{
case 1: //基于姓名删除
{
char n[10];
printf("\n请输入学生姓名:");
scanf("%s",n);
delete_by_name(S, n);
}break;
case 2: //基于学号删除
{
int n;
printf("\n请输入学生学号:");
scanf("%d",&n);
delete_by_id(S, n);
}break;
default :printf("选项不存在\n");break;
}
}break;
case 3: //改
{
printf("1:基于姓名修改\n");
printf("2:基于学号修改\n");
printf("请选择您要使用的功能分支 >>> ");
int n3;
scanf("%d",&n3);
switch(n3)
{
case 1: //基于姓名修改
{
char arr[10];
printf("请输入姓名>>>");
scanf("%s",arr);
updata_name(S, arr);
}break;
case 2: //基于学号修改
{
int id;
printf("请输入学号>>>");
scanf("%d",&id);
updata_id(S, id);
}break;
default :printf("选项不存在\n");break;
}
}break;
case 4: //查
{
printf("基于姓名查找\n");
printf("基于学号查找\n");
printf("基于成绩查找\n");
printf("请选择您要使用的功能分支 >>> ");
int n4;
scanf("%d",&n4);
switch(n4)
{
case 1: //基于姓名查找
{
char arr[10];
printf("请输入姓名>>>");
scanf("%s",arr);
select_by_name(S, arr);
}break;
case 2: //基于学号查找
{
int n;
printf("请输入学号>>>");
scanf("%d",&n);
}break;
case 3: //基于成绩查找
{
float n;
printf("请输入成绩>>>");
scanf("%f",&n);
}break;
default :printf("选项不存在\n");break;
}
}break;
case 5: //遍历链表,查看学生信息
{
list_show(S);
}break;
case 6: //学号顺序排列
{
sort(S);
}break;
case 0:
{
exit (0);
}break;
default :printf("编号输入有误,请重新输入\n");break;
}
}
return 0;
}
接下来是其中用到的函数
首先是最早的创建节点函数,与一般情况从的操作并无不同。
//创建头节点
Linklist *create()
{
Linklist *S = (Linklist*)malloc(sizeof(Linklist));
if( NULL==S )
{
printf("头节点创建失败\n");
return NULL;
}
//初始化
S->next = NULL;
S->len = 0;
printf("创建成功\n");
return S;
}
但是在申请子节点空间时就要注意了,在Linklist结构体中的共用体内的结构体指针,在使用malloc函数时,并不会自动帮你申请空间。打个比方,这个指针相当于一张小纸片,上面写着你公司所在的地址,但是在malloc函数里,这张纸所占用的空间,也只是一张纸大小罢了。所以,你需要手动调用malloc函数去申请它指向的这个结构体所占用的空间。
//申请子节点
Linklist *node_add( STU student)
{
Linklist *p = (Linklist*)malloc(sizeof(Linklist));
p->info = (STU*)malloc(sizeof(STU)); //手动申请共用体内结构体指针指向的结构体所占用的空间
if(NULL == p || NULL == p->info)
{
printf("子节点申请失败\n");
}
//保存数据
*(p->info) = student;
p->next = NULL;
//printf("节点申请成功\n");
return p;
判空函数最简单,只需要一个三目运算符构成的语句即可
//判空
int empty(Linklist *S)
{
return S->next == NULL ? 1:0;
}
因为学生信息表,一般信息直接添加在最后,故处于方便考虑,我只提供尾插法来执行增加信息功能
//增加学生信息(默认尾插,你别无选择)
int list_intsrt_tail( Linklist *S, STU student )
{
if(NULL == S)
{
printf("所给链表不合法\n");
return -1;
}
Linklist *p = node_add(student);
Linklist *q = S;
while(q->next != NULL)
{
q = q->next;
}
q->next = p;
//表的变化
S->len++;
}
当插入学生信息后,因为之后的代码会越来越长,我们需要边写代码遍检查代码的正确性,于是,我们需要一个遍历函数,以便时刻监视链表内的数据变化是否符合预期
//遍历
void list_show(Linklist *L)
{
//判断逻辑
if(NULL==L || empty(L)) //判断链表是否存在以及是否为空
{
printf("遍历失败\n");
return ;
}
//遍历
printf("链表元素分别是:\n");
Linklist *q = L->next; //从第一个元素开始
while(q != NULL) //到最后一个元素结束
{
printf("%s\t%d\t%.1f\t\n",q->info->name, q->info->id, q->info->score);
q=q->next;
}
printf("\n"); //处于美观性考虑,我空了一行出来
}
至此,我们完成了增删改查四大模块中的“增”。下一步是“删”
删除功能依据判定的变量不同,有两个方向,基于姓名和基于学号,但是总体来说,两个函数极为相似,只是基于的变量不同罢了。它们的函数发挥作用,还需要两个辅助模块:寻找前一个节点地址以及任意位置删除函数,这两个模块组合在一起,形成了删除功能的核心模块
//寻找前一个位置地址,寻址函数
Linklist *find(Linklist *S, Linklist *p)
{
Linklist *q = S; //让指针q指向头节点
while(q->next != p) //让指针指到目标节点的前一个节点处停下
{
q = q->next;
}
return q; //返回其地址
}
//任意位置删除
void delete_everypos(Linklist *S, Linklist *p)
{
Linklist *q = find(S, p); //调用寻址函数,找到p指针所在节点的前一个节点
q->next = p->next; //让p的前一个节点的指针绕过p,指向p的后一个节点
p->next = NULL; //使p指向的节点断开与链表的连接
free(p); //释放p指向节点的空间,即删除了节点p
p = NULL; //让p指向NULL,防止其变成野指针
//表的变化
S->len--;
}
有了这两个函数组成的模块,删除就变得很简单了,我们要做的,只是遍历链表,一个一个节点比对,找出我们所需的节点,进行操作即可,随后进行的改、查函数也脱胎于此
//基于姓名删除
int delete_by_name(Linklist *S, char n[])
{
//逻辑判断
if(NULL == S || empty(S))
{
printf ("表非法,或表为空\n");
return -1;
}
Linklist *p = S->next;
//判定条件:没找到匹配的姓名,或者p的下一位不为空
for(int i=0; strcmp(p->info->name, n) != 0 && p->next != NULL; i++)
{
p = p->next;
}
//判断离开循环是因为找到了节点,还是跑完了链表
if(strcmp(p->info->name , n) == 0)
{
delete_everypos(S, p); //找到节点即调用删除模块删掉节点
printf("删除成功\n\n");
}else
{
printf("未找到该学生\n");
}
}
/********************************************/
//基于学号删除
int delete_by_id(Linklist *S, int n)
{
//逻辑判断
if(NULL == S || empty(S))
{
printf ("表非法,或表为空\n");
return -1;
}
Linklist *p = S->next;
//判定条件:没找到匹配的学号,或者p的下一位不为空
for(int i=0; p->info->id != n && p->next != NULL; i++)
{
p = p->next;
}
//判断离开循环是因为找到了节点,还是跑完了链表
if(p->info->id == n)
{
delete_everypos(S, p); //找到节点即调用删除模块删掉节点
printf("删除成功\n\n");
}else
{
printf("未找到学生\n");
}
}
改查的函数与删除函数的区别不大,在修改函数中,为了方便起见,我采用了结构体对结构体赋值的方式,可以减少操作步骤。在输入结构体内信息时,我们可以直接定义一个变量,在栈区申请空间,可以减少手动申请空间释放空间的繁琐操作。具体程序原理与删除函数大差不差,我在此就不过多赘述。
//基于姓名修改
int updata_name(Linklist *S, char arr[])
{
//逻辑判断
if(NULL == S || empty(S))
{
printf("表非法或表空,无法修改\n");
return -1;
}
Linklist *p = S->next;
//寻找
//判定条件:没找到匹配的姓名,或者p的下一位不为空
for(int i=0; strcmp(p->info->name, arr) != 0 && p->next != NULL; i++)
{
p = p->next;
}
if(strcmp(p->info->name , arr) == 0)
{
STU student;
printf("请输入姓名>>>");
scanf("%s",student.name);
printf("请输入学号>>>");
scanf("%d",&student.id);
printf("请输入成绩>>>");
scanf("%f",&student.score);
//直接结构体赋值
*(p->info) = student;
printf("\n修改完成\n\n");
}else
{
printf("未找到学生信息\n\n");
}
}
/**************************************************/
//基于学号修改
int updata_id(Linklist *S, int id)
{
//逻辑判断
if(NULL == S || empty(S))
{
printf("表非法或表空,无法修改\n");
return -1;
}
Linklist *p = S->next;
for(int i=0; p->info->id != id && p->next != NULL; i++)
{
p = p->next;
}
if(p->info->id == id)
{
STU student;
printf("请输入姓名>>>");
scanf("%s",student.name);
printf("请输入学号>>>");
scanf("%d",&student.id);
printf("请输入成绩>>>");
scanf("%f",&student.score);
//直接结构体赋值
*(p->info) = student;
printf("\n修改完成\n\n");
}
else
{
printf("未找到学生信息\n\n");
}
}
查找函数也没什么好说的,逻辑与其他两函数完全一致,本质上来说,删、改、查,都是要找到需要操作的节点,然后决定是将其抹除、将其修改、还是将其输出。这导致,它们只会在操作节点部分会有差别,主体函数则是基本一致。
以下是查找模块,不再赘述。
//基于姓名查找
int select_by_name(Linklist *S, char n[])
{
if(NULL == S || empty(S))
{
printf ("表非法,或表为空\n");
return -1;
}
Linklist *p = S->next;
//判定条件:没找到匹配的姓名,或者p的下一位不为空
for(int i=0; strcmp(p->info->name, n) != 0 && p->next != NULL; i++)
{
p = p->next;
}
if(strcmp(p->info->name , n) == 0)
{
printf("%s,学号为:%d,成绩为:%.1f",p->info->name, p->info->id, p->info->score);
}else
{
printf("未找到该学生\n");
}
}
/************************************************************/
//基于学号查找
int select_by_id(Linklist *S, int n)
{
if(NULL == S || empty(S))
{
printf ("表非法,或表为空\n");
return -1;
}
Linklist *p = S->next;
//判定条件:没找到匹配的姓名,或者p的下一位不为空
for(int i=0; p->info->id != n && p->next != NULL; i++)
{
p = p->next;
}
if(p->info->id == n)
{
printf("%s,学号为:%d,成绩为:%.1f",p->info->name, p->info->id, p->info->score);
}else
{
printf("未找到该学生\n");
}
}
然后是排序模块,其实按照我本来的构想,这个模块不是一个独立存在的功能,而是一个辅助函数,它将在增删改查四大功能的结束后,自动调用。这样可以保证链表内元素的有序。但是最后经过多方面的考虑,因为时间等因素,我放弃了这个方案,将其独立了出来。
以下是基于学号的升序排列,本制上不过是基础的冒泡排序法
//学号升序
void sort(Linklist *S) //双循环冒泡排序法罢了
{
Linklist *p = S->next;
for(int i=1; p->next == NULL; i++)
{
for(int j=0; p->next->next == NULL; j++)
{
if(p->info->id > p->next->info->id)
{ //用异或的方式交互值
p->info->id ^= p->next->info->id; // a ^= b;
p->next->info->id ^= p->info->id; // b ^= a;
p->info->id ^= p->next->info->id; // a ^= b;
}
p = p->next;
}
}
printf("排序完成\n");
}
最后我们还需要销毁链表,防止内存泄漏
总的来说,是靠循环使用头删法,最终释放头节点的空间来实现。
//释放链表
void free_list(Linklist* S)
{
Linklist* q = S->next; //定义指针指向第一个元素
Linklist* p = S; //定义指针指向头节点(也可以直接拿头节点操作)
for (int i = 0; p->next != NULL; i++) //循环使用头删法,直到链表为空
{
p->next = q->next; //孤立
q->next = NULL; //断开连接
free(q); //释放空间
q = p->next; //移动到下一个元素
}
free(S); //释放头节点空间,并让头节点指针指向NULL,防止变成野指针
S = NULL;
}
至此,一个完整的学生管理系统(丐版)就实现了