下面将以我之前做的小项目(ATM存取款系统)的部分代码来为大家介绍有点另类的双向循环链表。
结构体:
#ifndef __LIST_H___
#define __LIST_H___
#include
#include
#include
#include
#include
#include
#include
typedef struct
{
char name[20];//名字
char sex[10];//性别
char card[20];//账号
int age;//年龄
char phone[20];//电话
char bankcard[20];//银行卡号
char password[10];//密码
float balance;//余额
}student_t;
typedef struct node{
void *datap;//指向数据的指针
struct node *prev,*next;
}DNode_t;
//抽象链表线性表信息头
typedef struct{
int n; //数据的个数,可以说是链表的节点数(不包括头节点)
int size; //数据空间大小
DNode_t head; //链表头
}LIST_T;
可能一时间看不懂为什么这样写,这样写有什么好处呢?
typedef struct node{
char name[20];
char sex[10];
char card[20];
int age;
char phone[20];
char bankcard[20];
char password[10];
float balance;
struct node *prev,*next;
}LIST_T;
大部分人初学者写链表结构体是上面这样写的,把链表跟要插入链表的详细数据弄在一起操作,但是有没有想过这样写,会把它给写死了,就是如果我突然不想插入下面的信息到链表里面,现在只需要把一个整型数插入到链表里面,那是不是要把整个链表给改了才行。改成下面这样:
typedef struct node{
int num;
struct node *prev,*next;
}LIST_T;
所有说,换种数据插入到链表里,又要把整个链表结构体给改了,那为什么不把链表结构体写的通用性一些呢。现在应该知道的链表结构体为什么是下面这样了吧。
typedef struct node{
void *datap;//指向数据的指针
struct node *prev,*next;
为什么说这样写比较通用呢?
首先void *datap,它是一个万能指针,可以存放任意类型变量的地址,给它分配要存储的数据大小内存,把要存储的数据拷贝到内存里就行了,也就是它指向了你存储数据的地址,而且存储什么类型都可以,无需大改,只需要操作数据大小、数据赋值和拷贝数据我这里的数据就是一开始定义的student_t结构体,你们自己定义,把它单独弄出来,上面这个结构体不需要改动,后面代码会陆续讲到。
注意:万能指针不能直接解引用,要强制转化为存储数据的类型,在进行操作。
我们看下面的代码
//抽象链表线性表信息头
typedef struct{
int n; //数据的个数
int size; //数据空间大小
DNode_t head;//链表头
}
这个又为什么这样写呢?首先我们要知道数据空间的大小才能分配内存,数据赋值是用户做的事,我们只管分配内存、拷贝数据到内存、插入链表里,所以我们要数据空间的大小,如果把它弄到上一个结构体里面,封装性就不好了,那样每次都要改结构体,还把数据空间大小插入链表里头,而且每次都要赋值。所以我们可以定义一个结构体,把链表抽象出来,弄到一个结构体里面,就是把链表和描述链表的信息放在一个结构体里面,初始化一次结构体就行了,这样方便好多,还添加了一个数据个数,代表着有多少节点(不包括头节点),这样就可以马上知道有多少人注册了账号等等,不需要遍历来求出个数,是不是很方便?
下面将以插入信息和遍历,来为大家解释,因为代码量有点大不好解释,所以只讲一小部分代码,请大家见谅!!!。
list.h
typedef struct
{
char name[20];
char sex[10];
char card[20];
int age;
char phone[20];
char bankcard[20];
char password[10];
float balance;
}student_t;
typedef struct node{
void *datap;//指向数据的指针
struct node *prev,*next;
}DNode_t;
//抽象链表线性表信息头
typedef struct{
int n; //数据的个数
int size; //数据空间大小
DNode_t head;//链表头
}LIST_T;
//学生信息
int initStu(student_t *s);
//初始化空的链表线性表
LIST_T *initList(int size);
//头部追加数据
int appendListHead(LIST_T *ptr,const void *datap);
//遍历
void travelListNext(LIST_T *ptr,void (*show)(void *datap));
//总数
int lengthList(LIST_T *ptr);
//回收资源
void destoryList(LIST_T *ptr);
//打印某个节点的信息
void showStu(void *datap)
//打印整数
void shownum(void *datap);
list.c
#include"list.h"
//初始化空的链表线性表
LIST_T *initList(int size)//size数据空间的大小,你要根据你要存储数据大小来定义
{
LIST_T *tmp;
tmp=malloc(sizeof(LIST_T));//分配内存空间
if(NULL==tmp) return NULL;
tmp->n =0;//节点个数(不包括头节点)
tmp->size=size;//数据空间大小
//头节点 自己指向自己
tmp->head.next=&tmp->head;
tmp->head.prev=&tmp->head;
return tmp;
}
//单纯插入 把插入弄出来,那样头部插入、尾部插入、有序插入都可以用代码更加的简洁明了
static void __insertEntry(DNode_t *new,DNode_t *P,DNode_t *N)
{
new->next=N;
new->prev=P;
N->prev=new;
P->next=new;
}
//头部插入
int appendListHead(LIST_T *ptr,const void *datap)
{
int ret;
DNode_t *one;//定义新节点
//分配数据节点空间 为链表结构体分配空间
one=malloc(sizeof(DNode_t));
if(one==NULL) return -1;
//分配数据空间 为你要存储的数据数据分配空间并让万能指针void *datap指向
one->datap=malloc(ptr->size);
if(one->datap==NULL)
{
free(one);
return -2;
}
//数据复制
memcpy(one->datap,datap,ptr->size);
//将该数据节点插入到链表的头节点后
__insertEntry(one,&ptr->head,ptr->head.next);
//数据个数加1
ptr->n=ptr->n+1;
return 0;
}
///遍历///
void travelListNext(LIST_T *ptr,void (*show)(void *datap))
{
DNode_t *t;
t=ptr->head.next;
while(t!=&ptr->head)
{
show(t->datap);
t=t->next;
}
}
//注册账号
int initStu(student_t *s)
{
int ret,i;
char buf[10]={0};
printf("请输入相关信息\n");
printf("请输入姓名:");
scanf("%s",s->name);
printf("请输入性别:");
scanf("%s",s->sex);
printf("请输入身份证卡号:");
scanf("%s",s->card);
printf("请输入年龄:");
scanf("%d",&s->age);
printf("请输入电话号码:");
scanf("%s",s->phone);
printf("注册成功\n");
//随机产生19位的银行卡号
memset(s->bankcard,0,sizeof(s->bankcard));
for(int i=0;i<19;i++)
*(s->bankcard+i)=rand()%10+'0';
printf("你的卡号为:");
printf("%s\n",s->bankcard);
system("stty -icanon");//关闭缓存区
system("stty -echo");//关闭回显 就是键盘输入不显示出来 实现输入密码是*
while(getchar()!='\n');
do{
printf("设置6位登陆密码:");
for(i=0;i<6;i++)
{
scanf("%c",&s->password[i]);
printf("*");
}
s->password[6]='\0';
ret=strlen(s->password);
if(ret!=6)
{
printf("设置失败,请重新设置!!!\n");
continue;
}
}while(ret!=6);
while(1)
{
do{
printf("再次输入登陆密码:");
for(i=0;i<6;i++)
{
scanf("%c",buf+i);
printf("*");
}
buf[6]='\0';
ret=strlen(buf);
if(ret!=6)
{
printf("输入失败,请输入设置!!!\n");
continue;
}
}while(ret!=6);
int ret=strcmp(s->password,buf);
if (ret==0)
break;
printf("密码不一致:\n");
}
system("stty icanon");//打开缓存
system("stty echo");//打开回现
printf("恭喜你!!!,完成注册手续。\n");
s->balance=0;
printf("\n");
return 0;
}
//打印某个节点的信息
void showStu(void *datap)
{
student_t *s=(student_t *)(datap);
printf("姓名:%s 性别:%s 身份证号:%s 年龄:%d 电话号码:%s 银行卡号:%s 登陆密码:%s 余额:%f\n",
s->name,s->sex,s->card,s->age,s->phone,s->bankcard,s->password,s->balance);
}
void shownum(void *datap)
{
int *num=(int *)(datap);
printf("num:%d\n",*num);//打印一个整型数
}
//数据个数
int lengthList(LIST_T *ptr)
{
return ptr->n;
}
//回收资源
void destoryList(LIST_T *ptr)
{
DNode_t *t,*next;
t=ptr->head.next;
while(t!=&ptr->head)
{
next=t->next;
free(t);
t=next;
}
free(ptr);
}
main.c
//单纯的插入、遍历
#include "list.h"
int main(int agrc,char **agrv)
{
system("clear");//清屏
int k,ret;
//可以定义两条链表 要看你自己的需求 一条结构体数据,一条整型数 下面简述只能一条要么结构体,要么整数
LIST_T *intp;//抽象链表线性表信息头
//LIST_T *ptr;//抽象链表线性表信息头
student_t stu;//定义数据结构体
///
#if 1 //插入数据结构体信息到链表
intp = initList(sizeof(student_t));//初始化抽象链表线性表信息头 参数是你要存储数据的大小 这里是list.h里的结构体
#elif 0
intp = initList(sizeof(int));//初始化抽象链表线性表信息头 参数是你要存储数据的大小 这里是一个整型
#endif
if(intp==NULL)
{
fprintf(stderr,"init list failed.\n");
return 1;
}
while(1)
{
#if 1 //插入数据结构体信息到链表
if(initStu(&stu)==-1) break;//对数据结构体赋值
ret=appendListHead(intp,&stu);//插入到链表里头 就是将数据拷贝到所void *datap所分配的数据空间并插入链表 头部插入
#elif 0 //插入一个整型数到链表
int num;
if((ret=scanf("%d",&num))!=1) break;//对整型数据赋值
ret=appendListHead(intp,&num);//插入到链表里头 就是将数据拷贝到所void *datap所分配的数据空间并插入链表 头部插入
#endif
if(ret!=0)
{
fprintf(stderr,"append data to list failed.\n");
break;
}
k=lengthList(intp);//直接获取结构体intp里n(数据个数)就行了,不用遍历来查看个数
printf("卡号个数:%d\n",k);//卡号个数
///遍历
#if 1
//结构体
travelListNext(intp,showStu);//查询全部信息,也就是全部节点
#elif 0
//整型数
travelListNext(intp,shownum);//查询全部信息,也就是全部节点
#endif
}
//回收资源
destoryList(intp);
}
上面一个插入.h里的数据结构体到链表,一个是插入整型数到链表,只要重新初始化抽象链表线性表信息头和改变数据操作就行,例如:遍历,遍历函数不用变,只需变第二个参数(对应数据的显示函数),初始化抽象链表线性表信息头时改数据空间大小等。
可以有些人对遍历函数void travelListNext(LIST_T *ptr,void (*show)(void *datap));不太理解,下面为大家解释一下:
void travelListNext(LIST_T *ptr,void (*show)(void *datap))
{
DNode_t *t;
t=ptr->head.next;
while(t!=&ptr->head)
{
show(t->datap);//参数是:指向数据的万能指针void *datap
t=t->next;
}
}
//打印某个节点的信息
void showStu(void *datap)
{
student_t *s=(student_t *)(datap);//因为是万能指针不能直接操作,要强制转换成存储的数据类型
printf("姓名:%s 性别:%s 身份证号:%s 年龄:%d 电话号码:%s 银行卡号:%s 登陆密码:%s 余额:%f\n", s->name,s->sex,s->card,s->age,s->phone,s->bankcard,s->password,s->balance);
}
首先,定一个参数是你的抽象链表线性表信息头,第二个是函数指针:存储函数地址的指针变量,跟他的返回值类型和参数类型都没关系,存放你要显示数据的函数,你要怎么显示你的数据只要改你的显示函数就行,想怎么显示就怎么显示。例如:travelListNext(intp,showStu);
上面大部分函数的参数void *类型的,知道为什么吗?
你想想如果你弄成了int *,那么是不是只能传int *类型过去,想传其他类型是不是要强制转换,比如你要传类字符串过去,你是不是要对字符串强制转换成int *。
所以要写成void *,因为这样写通用性很强,void *存放任何类型地址。你们要记住封装函数时,不要写死了,想想怎么写,别人也能用到你的函数。
在这里我在提一点:我们插入到链表或者查询链表有没有那个数据等等,是不是每次调用函数后要判断成不成功,如果多处地方调用是不是都要写判断是否成功,弄得代码不够简洁,我们可以把所函数调用跟判断再弄成一个函数,那样直接调用函数就行了,多处地方调用也只需一个函数,是不是简洁很多。你们可以试一试。
最后我布置一道练习,编写一个查找函数,就是查找链表有没有匹配的数据,有就返回数据空间地址(如果多个数据匹配只返回第一个),没有就返回NULL,大家试写一下。
提示:
/*
*compare_t 比较函数类型
*d1 数据大于 d2 返回大于0 的值
*d1 数据等于 d2 返回等于0 的值
*d1 数据小于 d2 返回小于0 的值
*/
typedef int compare_t(const void *d1,const void *d2);
//compFunc比较函数指针 第一个参数为链表中的数据指针 第二参数为key查找关键字
void *searchOneListByCond(LIST_T *ptr,compare_t *compFunc,const void *key);
例子:
typedef int compare_t(const void *d1,const void *d2) ;
compare_t fun;//其实就是int fun(const void *d1,const void *d2);
.
typedef void func_t(char *)
func_t sayHello; //void sayHello(char *name);
.
typedef int compare_t(const void *d1,const void *d2) ;
所以compare_t *compFunc其实就是定义了存放参数是(const void *d1,const void *d2),返回值是int 类型的函数地址的函数指针。这个函数用来比较的,如果数据匹配返回对应的值。
提示就到这里啦,大家有空可以写写。
全篇完。
本人博客仅仅代表我个人见解方便记录成长笔记。
若有与 看官老爷见解有冲突,我坚信看官老爷见解是对的,我的是错的。
感谢~!