前言
在工作中经常会使用到链表作为动态的缓存队列。麻烦的是,链表中的数据部分并不是一成不变的,有时候是一个基本类型(如int、float等);有时候是复杂的结构体。这导致每次都要重新修改数据的赋值操作甚至重新写一个链表,代码重用率很低。那么,为什么不写一个通用的链表来适应不同的数据类型呢?
定义一个通用的变量类型
我们知道,链表的节点包括数据域和指针域,如:
typedef int DATETYPE;
typedef struct node_list
{
DATETYPE data;
struct node_list *prev;//指向前一个节点
struct node_list *next;//指向后一个节点
}NodeList;
指针域的操作在所有的链表中都是通过,但是数据域因为类型不同赋值的方法不同。所以我们需要把数据域的数据类型统一化。
实际上,对一个变量赋值,就是对变量所在的地址写入相应的值。例如在下面的例子中,*p = 12 和 a = 12效果是一样的。
int a;
int *p = &a;//获取a的地址
*p = 12;//给地址写入12
a = 12;
假设上面例子中int占用4个字节,当a变量定义后系统会分配4个字节的空间用来储存a的值,并且给出a的地址(&a)。给a赋值后,这个连续的地址上的值变成 0x00 0x00 0x00 0x0c(根据不同计算机字节序不同,赋值后顺序不同)。因此,只要知道变量的大小和首地址,我们就直接对地址操作,而不需要知道变量类型了。 所以,上面的结构体改为:
typedef struct node_list
{
void *data;
struct node_list *prev;//指向前一个节点
struct node_list *next;//指向后一个节点
}NodeList;
这里定义void型指针来指向变量的地址。还有一个表示变量大小的变量,因为一个链表中的数据类型是相同的,所以变量大小不需要在每个节点中都定义,这里再定义整体的结构体用来表示整个链表:
typedef struct
{
NodeList *head;
int data_len;//表示变量大小
}GenList;
head表示链表的头节点,指向整个链表的数据;data_len表示数据域变量的大小。
完成链表操作
1.初始化GenList
首先,我们根据项目需要,自定义一个数据类型,例如:
struct Type
{
int id;
char data[17];
int num;
};
接下来是定义GenList变量list并初始化:将自定义的Type结构体大小(sizeof(struct Type))告诉list,并申请头节点,头节点用来接连链表的头和尾,链表空时,头和尾指针都指向自身。头结点不保存数据:
GenList list;
ret = gen_list_init(&list,sizeof(struct Type));
int gen_list_init(GenList *list, int dataLen)
{
NodeList *head = (NodeList*)malloc(sizeof(NodeList));
if(!head)
return LIST_STA_MEM_ERR;//返回错误码
list->head = head;
list->head->prev = list->head->next = head;
list->data_len = dataLen;
return LIST_STA_OK;
}
初始化后list变成:
2.向链表尾部添加一个节点
定义一个struct Type的变量并赋值,调用add_list_tail将该数据添加到链表中:
struct Type d = {12,"hello",43};
ret = add_list_tail(&list, &d);
int add_list_tail(GenList *list, void *data)
{
NodeList *head,*p;
head = list->head;//获得头节点
p = head->prev;//p指向尾节点
NodeList *new = (NodeList*)malloc(sizeof(NodeList));//申请一个新的节点
if(new)
{
new->data = malloc(list->data_len);//为数据域申请空间,大小与自定义的类型相等
if (!new->data)
{
free(new);
return LIST_STA_MEM_ERR;
}
memcpy(new->data, data, list->data_len);
p->next = new;
new->prev = p;
new->next = head;
head->prev = new;
return LIST_STA_OK;
}
return LIST_STA_MEM_ERR;
}
链表节点的数据域我们定义的是一个void型指针,必须给它分配内存才能写入数据,分配的内存大小根据自定义的类型大小来决定,即:sizeof(struct Type),初始化时已经将sizeof(struct Type)赋值给了data_len,因此,申请了data_len个字节的内存。
调用add_list_tail函数时,参数data是指向变量d的首地址,从这个地址开始的连续data_len个地址上保存的就是变量d的值。所以,我们直接通过memcpy函数,把数据复制到我们申请的新节点数据域上。
根据上述代码,向链表尾部添加一个节点后,list变成:
2.从链表尾部读出节点数据
和其他的链表一样,我们可以很容易的读出一个节点。对于队列先进先出操作,一般是向尾部添加节点而从头部读出节点。例如下面从头部读出节点:
void* get_list_data_head(GenList list)
{
return (list.head->next->data);
}
返回的数据是void*,需要转换成我们自定义的类型:
struct Type d = *(struct Type*)get_list_data_head(list);
printf("***id:%d,num:%d,data:%s\n",d.id,d.num,d.data);
3.删除链表尾部出节点
读出数据后,就可以删除该节点了,上面是从链表头读取的数据,所以删除头节点:
int del_list_tail(GenList *list)
{
NodeList *head,*p;
head = list->head;//获得头节点
p = head->prev;//p指向尾节点
if(p == head)
return LIST_STA_LIST_NULL;
if(p->data)
free(p->data);//释放数据域申请的内存
p->prev->next = p->next;
p->next->prev = p->prev;
p->prev = NULL;
p->next = NULL;
free(p);//释放节点申请的内存
p = NULL;
return LIST_STA_OK;
}
至此,一个链表的创建、添加、读取、删除已经完成了!
以下是完整的代码,包含库文件gen_list.c gen_list.h和示例代码test.c:gen_list